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