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:
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, h } from 'vue'
|
||||
import { NDataTable, NButton, NTag, NAlert, NCard, NGrid, NGridItem, NPopover } from 'naive-ui'
|
||||
import { api } from '../../api/client'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
|
||||
const toast = useToast()
|
||||
const runs = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(true)
|
||||
|
||||
// Action loading states
|
||||
const running = ref(false)
|
||||
const refreshingCitations = ref(false)
|
||||
const extractingPico = ref(false)
|
||||
const backfillingDesign = ref(false)
|
||||
const backfillingOA = ref(false)
|
||||
|
||||
function fmt(ts: string | null | undefined): string {
|
||||
if (!ts) return '—'
|
||||
const d = new Date(ts)
|
||||
if (isNaN(d.getTime())) return ts
|
||||
return d.toLocaleString('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
hour12: false,
|
||||
}).replace(/\//g, '-')
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await api.get('/admin/pipeline/runs', { params: { page_size: 50 } })
|
||||
runs.value = data.items || []
|
||||
total.value = data.total || 0
|
||||
} catch (e) { toast.apiError(e, '加载管道记录失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function triggerPipeline() {
|
||||
running.value = true
|
||||
try {
|
||||
await api.post('/admin/pipeline/run', { params: { mode: 'ftp' } })
|
||||
toast.success('FTP 增量管道已触发')
|
||||
await loadData()
|
||||
} catch (e) { toast.apiError(e, '触发失败') }
|
||||
finally { running.value = false }
|
||||
}
|
||||
async function refreshCitations() {
|
||||
refreshingCitations.value = true
|
||||
try { const { data } = await api.post('/admin/pipeline/refresh-citations'); toast.success('引用更新完成: ' + data.updated + ' 篇'); await loadData() }
|
||||
catch (e) { toast.apiError(e, '刷新失败') }
|
||||
finally { refreshingCitations.value = false }
|
||||
}
|
||||
async function extractPico() {
|
||||
extractingPico.value = true
|
||||
try { const { data } = await api.post('/admin/pipeline/extract-pico'); toast.success('PICO 提取完成: ' + data.processed + ' 篇'); await loadData() }
|
||||
catch (e) { toast.apiError(e, '提取失败') }
|
||||
finally { extractingPico.value = false }
|
||||
}
|
||||
async function backfillDesign() {
|
||||
backfillingDesign.value = true
|
||||
try { const { data } = await api.post('/admin/pipeline/backfill-study-designs'); toast.success('回填完成: ' + data.updated + ' 篇'); await loadData() }
|
||||
catch (e) { toast.apiError(e, '回填失败') }
|
||||
finally { backfillingDesign.value = false }
|
||||
}
|
||||
async function backfillOA() {
|
||||
backfillingOA.value = true
|
||||
try { const { data } = await api.post('/admin/pipeline/backfill-oa-text?limit=5000'); toast.success('OA全文回填完成: ' + data.fetched + ' 篇'); await loadData() }
|
||||
catch (e) { toast.apiError(e, '回填失败') }
|
||||
finally { backfillingOA.value = false }
|
||||
}
|
||||
|
||||
function fmtDuration(started: string | null, completed: string | null): string {
|
||||
if (!started || !completed) return '—'
|
||||
const s = new Date(started).getTime()
|
||||
const e = new Date(completed).getTime()
|
||||
if (isNaN(s) || isNaN(e)) return '—'
|
||||
const sec = Math.round((e - s) / 1000)
|
||||
if (sec < 60) return sec + '秒'
|
||||
return Math.floor(sec / 60) + '分' + (sec % 60) + '秒'
|
||||
}
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
daily_ftp_update: '📡 FTP 增量',
|
||||
daily_pubmed_pipeline: '🔍 精搜(旧)',
|
||||
weekly_broad_pipeline: '🌐 宽搜(旧)',
|
||||
daily_mesh_retagger: '🏷️ Retagger(旧)',
|
||||
baseline_import: '📦 基线导入',
|
||||
manual_full: '⚡ 手动全量',
|
||||
manual_majr: '⚡ 手动精搜',
|
||||
manual_broad: '⚡ 手动宽搜',
|
||||
manual_full_epmc: '⚡ EPMC 全量',
|
||||
daily_citation_update: '📊 引用刷新',
|
||||
}
|
||||
|
||||
const cols = [
|
||||
{
|
||||
title: '类型', key: 'type', width: 110, fixed: 'left' as const,
|
||||
render: (r: any) => h(NTag, { size: 'tiny', type: r.type === 'daily_ftp_update' ? 'success' : 'default' },
|
||||
() => typeLabels[r.type] || r.type),
|
||||
},
|
||||
{
|
||||
title: '状态', key: 'status', width: 60,
|
||||
render: (r: any) => h(NTag, { size: 'tiny', type: r.status === 'success' ? 'success' : 'error' },
|
||||
() => r.status === 'success' ? '✅' : '❌'),
|
||||
},
|
||||
{ title: '开始', key: 'started', width: 130, render: (r: any) => fmt(r.started) },
|
||||
{ title: '完成', key: 'completed', width: 130, render: (r: any) => fmt(r.completed) },
|
||||
{
|
||||
title: '耗时', key: 'duration', width: 70,
|
||||
render: (r: any) => fmtDuration(r.started, r.completed),
|
||||
},
|
||||
{ title: '文件数', key: 'files_processed', width: 60 },
|
||||
{ title: '总文献', key: 'total_articles', width: 60 },
|
||||
{ title: '➕新增', key: 'articles_new', width: 60 },
|
||||
{ title: '🔄更新', key: 'articles_updated', width: 60 },
|
||||
{ title: '🗑️删除', key: 'articles_deleted', width: 60 },
|
||||
{ title: '🚫过滤', key: 'articles_filtered', width: 60 },
|
||||
{ title: '📨Feed', key: 'feeds_generated', width: 60 },
|
||||
{
|
||||
title: '检查点', key: 'processed_date', width: 90,
|
||||
render: (r: any) => r.processed_date || '—',
|
||||
},
|
||||
{
|
||||
title: '元数据', key: 'metadata', width: 140, ellipsis: true,
|
||||
render: (r: any) => {
|
||||
if (!r.metadata) return '—'
|
||||
const parts: string[] = []
|
||||
if (r.metadata.ftp_year) parts.push('年份:' + r.metadata.ftp_year)
|
||||
if (r.metadata.last_sequence) parts.push('序号:' + r.metadata.last_sequence)
|
||||
if (r.metadata.year) parts.push('基线:' + r.metadata.year)
|
||||
return parts.join(' ')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '错误', key: 'error', width: 100, ellipsis: true,
|
||||
render: (r: any) => {
|
||||
if (!r.error) return '—'
|
||||
return h(NPopover, { trigger: 'hover' }, () => h('pre', { style: 'max-width:400px;font-size:12px;white-space:pre-wrap;margin:0' }, r.error))
|
||||
},
|
||||
},
|
||||
]
|
||||
const scrollX = 1350
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pipeline-view">
|
||||
<div class="header-row">
|
||||
<div class="header-left">
|
||||
<h2>📡 数据管道</h2>
|
||||
<span class="total-badge">共 {{ total }} 次运行</span>
|
||||
</div>
|
||||
<NButton type="primary" :loading="running" @click="triggerPipeline" class="sky-btn">🔄 FTP 增量更新</NButton>
|
||||
</div>
|
||||
|
||||
<NAlert type="info" style="margin-bottom:16px" title="管道架构(FTP 每日增量)">
|
||||
<div style="font-size:13px;line-height:1.8">
|
||||
• 🟢 默认管道:每天 03:07 UTC FTP 下载当日更新文件 → 解析 → 过滤 → upsert<br>
|
||||
• 2026-07-25 已从 E-utilities API 多路搜索切换为 FTP 每日更新文件<br>
|
||||
• 旧管道(精搜 MAJR / 宽搜 / retagger)标记为 @deprecated,仅保留兼容
|
||||
</div>
|
||||
</NAlert>
|
||||
|
||||
<!-- Action cards -->
|
||||
<n-grid :cols="4" :x-gap="12" responsive="screen" style="margin-bottom:20px">
|
||||
<n-grid-item>
|
||||
<NCard size="small" style="text-align:center">
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">📊 刷新被引次数</div>
|
||||
<NButton size="tiny" :loading="refreshingCitations" @click="refreshCitations" class="sky-btn">执行</NButton>
|
||||
</NCard>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<NCard size="small" style="text-align:center">
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">📝 PICO 提取</div>
|
||||
<NButton size="tiny" :loading="extractingPico" @click="extractPico" class="sky-btn">执行</NButton>
|
||||
</NCard>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<NCard size="small" style="text-align:center">
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">🏗️ 研究设计回填</div>
|
||||
<NButton size="tiny" :loading="backfillingDesign" @click="backfillDesign" class="sky-btn">执行</NButton>
|
||||
</NCard>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<NCard size="small" style="text-align:center">
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">📄 OA 全文回填</div>
|
||||
<NButton size="tiny" :loading="backfillingOA" @click="backfillOA" class="sky-btn">执行</NButton>
|
||||
</NCard>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<!-- Run history table -->
|
||||
<NCard title="运行历史" size="small">
|
||||
<NDataTable
|
||||
:columns="cols"
|
||||
:data="runs"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:max-height="600"
|
||||
:scroll-x="scrollX"
|
||||
:single-line="false"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pipeline-view {
|
||||
padding: 16px;
|
||||
}
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.header-left h2 {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.total-badge {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--border-color);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.pipeline-view { padding: 12px; }
|
||||
.header-row { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.n-grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user