Files
34047007@qq.com 62ca8fa6b8
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
chore: batch commit remaining changes
Includes search engine improvements, Alembic migrations,
new services (pubmed_daily_update, query_expansion),
frontend updates, and documentation sync.
2026-07-27 08:35:12 +08:00

242 lines
8.8 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>