AutoMedinfo/frontend/src/views/inquiry/DownloadTasks.vue

485 lines
14 KiB
Vue
Raw 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.

<template>
<div class="download-tasks">
<el-card v-loading="loading">
<template #header>
<div class="card-header">
<span>待下载任务</span>
<div>
<el-button @click="handleRefresh" icon="Refresh" :loading="loading">
刷新
</el-button>
<el-button @click="goBack" text>返回</el-button>
</div>
</div>
</template>
<!-- 统计信息 -->
<el-row :gutter="20" style="margin-bottom: 20px;">
<el-col :span="6">
<el-statistic title="待下载任务" :value="pendingTasks.length" value-style="color: #E6A23C">
<template #suffix>个</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="下载中" :value="downloadingCount" value-style="color: #409EFF">
<template #suffix>个</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="已完成" :value="completedCount" value-style="color: #67C23A">
<template #suffix>个</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="失败" :value="failedCount" value-style="color: #F56C6C">
<template #suffix>个</template>
</el-statistic>
</el-col>
</el-row>
<!-- 说明信息 -->
<el-alert
title="关于自动化下载"
type="info"
:closable="false"
style="margin-bottom: 20px;"
>
<div>
此页面显示所有待下载的文献列表。可供自动化下载工具读取并执行下载任务。
<br />
下载完成后,请更新任务状态。
</div>
</el-alert>
<!-- 筛选器 -->
<div class="filters">
<el-radio-group v-model="filterStatus" @change="loadTasks">
<el-radio-button label="all">全部任务</el-radio-button>
<el-radio-button label="PENDING">待下载</el-radio-button>
<el-radio-button label="DOWNLOADING">下载中</el-radio-button>
<el-radio-button label="COMPLETED">已完成</el-radio-button>
<el-radio-button label="FAILED">失败</el-radio-button>
</el-radio-group>
<el-select
v-model="selectedInquiryId"
placeholder="筛选查询请求"
clearable
style="width: 300px; margin-left: 15px;"
@change="loadTasks"
>
<el-option
v-for="inquiry in uniqueInquiries"
:key="inquiry.id"
:label="`${inquiry.requestNumber} - ${inquiry.title}`"
:value="inquiry.id"
/>
</el-select>
</div>
<!-- 任务列表 -->
<el-table :data="displayTasks" border stripe style="margin-top: 20px;">
<el-table-column type="expand">
<template #default="{ row }">
<div class="expand-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID">
{{ row.id }}
</el-descriptions-item>
<el-descriptions-item label="查询请求ID">
<el-link
type="primary"
@click="goToInquiry(row.inquiryRequestId)"
>
#{{ row.inquiryRequestId }}
</el-link>
</el-descriptions-item>
<el-descriptions-item label="标题" :span="2">
{{ row.title }}
</el-descriptions-item>
<el-descriptions-item label="作者" :span="2">
{{ row.authors || 'N/A' }}
</el-descriptions-item>
<el-descriptions-item label="摘要" :span="2">
<div style="white-space: pre-wrap; max-height: 150px; overflow-y: auto;">
{{ row.summary || 'N/A' }}
</div>
</el-descriptions-item>
<el-descriptions-item label="DOI" v-if="row.doi">
<a :href="`https://doi.org/${row.doi}`" target="_blank" style="color: #409EFF;">
{{ row.doi }}
</a>
</el-descriptions-item>
<el-descriptions-item label="PMID" v-if="row.pmid">
<a :href="`https://pubmed.ncbi.nlm.nih.gov/${row.pmid}`" target="_blank" style="color: #409EFF;">
{{ row.pmid }}
</a>
</el-descriptions-item>
<el-descriptions-item label="NCT ID" v-if="row.nctId">
<a :href="`https://clinicaltrials.gov/study/${row.nctId}`" target="_blank" style="color: #409EFF;">
{{ row.nctId }}
</a>
</el-descriptions-item>
<el-descriptions-item label="来源链接" :span="2" v-if="row.sourceUrl">
<a :href="row.sourceUrl" target="_blank" style="color: #409EFF; word-break: break-all;">
{{ row.sourceUrl }}
</a>
</el-descriptions-item>
<el-descriptions-item label="文件路径" :span="2" v-if="row.filePath">
{{ row.filePath }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(row.createdAt) }}
</el-descriptions-item>
<el-descriptions-item label="下载时间" v-if="row.downloadedAt">
{{ formatDateTime(row.downloadedAt) }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
</el-table-column>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" min-width="300" show-overflow-tooltip />
<el-table-column prop="source" label="来源" width="150">
<template #default="{ row }">
<el-tag :type="getSourceTagType(row.source)" size="small">
{{ row.source }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="downloadStatus" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.downloadStatus)" size="small">
{{ getStatusText(row.downloadStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="下载链接" width="150">
<template #default="{ row }">
<el-button
v-if="row.sourceUrl"
type="primary"
size="small"
link
@click="openLink(row.sourceUrl)"
>
打开链接
</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div style="display: flex; flex-direction: column; gap: 5px;">
<el-button-group>
<el-button
size="small"
type="success"
@click="handleMarkComplete(row)"
:disabled="row.downloadStatus === 'COMPLETED'"
>
标记完成
</el-button>
<el-button
size="small"
type="danger"
@click="handleMarkFailed(row)"
:disabled="row.downloadStatus === 'COMPLETED'"
>
标记失败
</el-button>
</el-button-group>
<el-button
size="small"
@click="copyDownloadInfo(row)"
>
复制信息
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<el-empty
v-if="displayTasks.length === 0"
description="暂无下载任务"
style="margin-top: 40px;"
/>
<!-- 批量导出按钮 -->
<div style="margin-top: 20px; text-align: right;">
<el-button
type="primary"
@click="handleExportJson"
:disabled="displayTasks.length === 0"
>
导出为JSON供自动化工具使用
</el-button>
</div>
</el-card>
<!-- 标记完成对话框 -->
<el-dialog v-model="completeDialogVisible" title="标记下载完成" width="500px">
<el-form :model="completeForm" label-width="100px">
<el-form-item label="文件路径">
<el-input
v-model="completeForm.filePath"
placeholder="请输入下载后的文件路径"
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="completeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitComplete">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getPendingDownloads, markDownloadComplete, markDownloadFailed } from '@/api/searchResult'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const loading = ref(false)
const tasks = ref([])
const filterStatus = ref('all')
const selectedInquiryId = ref(null)
const completeDialogVisible = ref(false)
const currentTask = ref(null)
const completeForm = ref({
filePath: ''
})
const pendingTasks = computed(() => {
return tasks.value.filter(t => t.downloadStatus === 'PENDING')
})
const downloadingCount = computed(() => {
return tasks.value.filter(t => t.downloadStatus === 'DOWNLOADING').length
})
const completedCount = computed(() => {
return tasks.value.filter(t => t.downloadStatus === 'COMPLETED').length
})
const failedCount = computed(() => {
return tasks.value.filter(t => t.downloadStatus === 'FAILED').length
})
const displayTasks = computed(() => {
let filtered = tasks.value
// 按状态筛选
if (filterStatus.value !== 'all') {
filtered = filtered.filter(t => t.downloadStatus === filterStatus.value)
}
// 按查询请求筛选
if (selectedInquiryId.value) {
filtered = filtered.filter(t => t.inquiryRequestId === selectedInquiryId.value)
}
return filtered
})
const uniqueInquiries = computed(() => {
const inquiryMap = new Map()
tasks.value.forEach(task => {
if (!inquiryMap.has(task.inquiryRequestId)) {
inquiryMap.set(task.inquiryRequestId, {
id: task.inquiryRequestId,
requestNumber: `REQ${task.inquiryRequestId}`,
title: task.title?.substring(0, 30) || '未命名'
})
}
})
return Array.from(inquiryMap.values())
})
onMounted(() => {
loadTasks()
})
const loadTasks = async () => {
loading.value = true
try {
const data = await getPendingDownloads()
tasks.value = data || []
} catch (error) {
ElMessage.error('加载下载任务失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
loadTasks()
}
const handleMarkComplete = (row) => {
currentTask.value = row
completeForm.value.filePath = row.filePath || ''
completeDialogVisible.value = true
}
const submitComplete = async () => {
if (!completeForm.value.filePath) {
ElMessage.warning('请输入文件路径')
return
}
try {
await markDownloadComplete(currentTask.value.id, completeForm.value.filePath)
ElMessage.success('已标记为完成')
completeDialogVisible.value = false
loadTasks()
} catch (error) {
ElMessage.error('操作失败')
}
}
const handleMarkFailed = (row) => {
ElMessageBox.prompt('请输入失败原因(可选)', '标记失败', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '失败原因'
}).then(async ({ value }) => {
try {
await markDownloadFailed(row.id, value || '')
ElMessage.success('已标记为失败')
loadTasks()
} catch (error) {
ElMessage.error('操作失败')
}
}).catch(() => {})
}
const openLink = (url) => {
window.open(url, '_blank')
}
const copyDownloadInfo = (row) => {
const info = {
id: row.id,
title: row.title,
source: row.source,
sourceUrl: row.sourceUrl,
doi: row.doi,
pmid: row.pmid,
nctId: row.nctId
}
const text = JSON.stringify(info, null, 2)
navigator.clipboard.writeText(text).then(() => {
ElMessage.success('已复制到剪贴板')
}).catch(() => {
ElMessage.error('复制失败')
})
}
const handleExportJson = () => {
const exportData = displayTasks.value.map(task => ({
id: task.id,
inquiryRequestId: task.inquiryRequestId,
title: task.title,
authors: task.authors,
source: task.source,
sourceUrl: task.sourceUrl,
doi: task.doi,
pmid: task.pmid,
nctId: task.nctId,
downloadStatus: task.downloadStatus,
createdAt: task.createdAt
}))
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `download-tasks-${Date.now()}.json`
a.click()
URL.revokeObjectURL(url)
ElMessage.success('导出成功')
}
const goToInquiry = (inquiryId) => {
router.push(`/inquiry/${inquiryId}`)
}
const formatDateTime = (dateTime) => {
if (!dateTime) return '-'
return new Date(dateTime).toLocaleString('zh-CN')
}
const getStatusTagType = (status) => {
const typeMap = {
'PENDING': 'warning',
'DOWNLOADING': 'primary',
'COMPLETED': 'success',
'FAILED': 'danger',
'NOT_REQUIRED': 'info'
}
return typeMap[status] || 'info'
}
const getStatusText = (status) => {
const textMap = {
'PENDING': '待下载',
'DOWNLOADING': '下载中',
'COMPLETED': '已完成',
'FAILED': '失败',
'NOT_REQUIRED': '不需要'
}
return textMap[status] || status
}
const getSourceTagType = (source) => {
const typeMap = {
'内部数据': 'primary',
'知识库': 'success',
'知网': 'warning',
'ClinicalTrials.gov': 'danger',
'CNKI': 'warning'
}
return typeMap[source] || 'info'
}
const goBack = () => {
router.back()
}
</script>
<style scoped>
.download-tasks {
width: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.filters {
display: flex;
align-items: center;
margin: 20px 0;
}
.expand-content {
padding: 20px;
}
:deep(.el-statistic__content) {
font-size: 28px;
font-weight: 600;
}
</style>