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

522 lines
15 KiB
Vue

<template>
<div class="search-results">
<el-card v-loading="loading">
<template #header>
<div class="card-header">
<span>检索结果管理 - {{ inquiry.requestNumber }}</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="results.length">
<template #suffix>条</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="已纳入回复" :value="includedCount" value-style="color: #67C23A">
<template #suffix>条</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="需要下载" :value="downloadCount" value-style="color: #E6A23C">
<template #suffix>条</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="已删除" :value="deletedCount" value-style="color: #F56C6C">
<template #suffix>条</template>
</el-statistic>
</el-col>
</el-row>
<!-- 批量操作 -->
<div class="batch-actions">
<el-button
type="primary"
@click="handleBatchInclude"
:disabled="selectedResults.length === 0"
>
批量纳入回复
</el-button>
<el-button
type="warning"
@click="handleBatchDownload"
:disabled="selectedResults.length === 0"
>
批量标记下载
</el-button>
<el-button
type="primary"
@click="handleGenerateDownloadTasks"
:disabled="downloadSelectedIds.length === 0"
>
生成下载任务 ({{ downloadSelectedIds.length }})
</el-button>
<el-button
type="danger"
@click="handleBatchDelete"
:disabled="selectedResults.length === 0"
>
批量删除
</el-button>
<el-button
type="success"
@click="handleGenerateResponse"
:disabled="includedCount === 0"
:loading="generating"
>
生成回复 ({{ includedCount }}条)
</el-button>
</div>
<!-- 筛选器 -->
<div class="filters">
<el-radio-group v-model="filterType" @change="handleFilterChange">
<el-radio-button label="all">全部 ({{ results.length }})</el-radio-button>
<el-radio-button label="active">未删除 ({{ activeCount }})</el-radio-button>
<el-radio-button label="included">已纳入 ({{ includedCount }})</el-radio-button>
<el-radio-button label="download">需下载 ({{ downloadCount }})</el-radio-button>
<el-radio-button label="deleted">已删除 ({{ deletedCount }})</el-radio-button>
</el-radio-group>
<el-input
v-model="searchText"
placeholder="搜索标题或内容"
clearable
style="width: 300px; margin-left: 15px;"
prefix-icon="Search"
/>
</div>
<!-- 结果列表 -->
<el-table
ref="tableRef"
:data="filteredResults"
@selection-change="handleSelectionChange"
border
stripe
style="margin-top: 20px;"
>
<el-table-column type="selection" width="55" />
<el-table-column type="expand">
<template #default="{ row }">
<div class="expand-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="标题" :span="2">
{{ row.title }}
</el-descriptions-item>
<el-descriptions-item label="来源">
<el-tag :type="getSourceTagType(row.source)">{{ row.source }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="发表日期">
{{ row.publicationDate || 'N/A' }}
</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: 200px; 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="来源链接" v-if="row.sourceUrl" :span="2">
<a :href="row.sourceUrl" target="_blank" style="color: #409EFF;">
{{ row.sourceUrl }}
</a>
</el-descriptions-item>
</el-descriptions>
</div>
</template>
</el-table-column>
<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 label="状态" width="200">
<template #default="{ row }">
<div style="display: flex; flex-direction: column; gap: 5px;">
<el-tag v-if="row.includeInResponse" type="success" size="small">
已纳入回复
</el-tag>
<el-tag v-if="row.needDownload" type="warning" size="small">
需要下载
</el-tag>
<el-tag v-if="row.isDeleted" type="danger" size="small">
已删除
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<div style="display: flex; flex-direction: column; gap: 5px;">
<el-button-group>
<el-button
size="small"
:type="row.includeInResponse ? 'success' : 'default'"
@click="toggleInclude(row)"
:disabled="row.isDeleted"
>
{{ row.includeInResponse ? '已纳入' : '纳入回复' }}
</el-button>
<el-button
size="small"
:type="row.needDownload ? 'warning' : 'default'"
@click="toggleDownload(row)"
:disabled="row.isDeleted"
>
{{ row.needDownload ? '已标记' : '下载全文' }}
</el-button>
</el-button-group>
<el-button
size="small"
type="danger"
@click="handleDelete(row)"
:disabled="row.isDeleted"
>
标记错误并删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<el-empty
v-if="results.length === 0"
description="暂无检索结果"
style="margin-top: 40px;"
>
<el-button type="primary" @click="goBack">返回重新检索</el-button>
</el-empty>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { getInquiryDetail, generateResponse, generateDownloadTasks } from '@/api/inquiry'
import {
getSearchResults,
updateSearchResult,
batchUpdateSearchResults,
deleteSearchResult
} from '@/api/searchResult'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const generating = ref(false)
const inquiry = ref({})
const results = ref([])
const selectedResults = ref([])
const downloadSelectedIds = computed(() => selectedResults.value.filter(r => r.needDownload && !r.isDeleted).map(r => r.id))
const filterType = ref('active')
const searchText = ref('')
const tableRef = ref(null)
const includedCount = computed(() => {
return results.value.filter(r => r.includeInResponse && !r.isDeleted).length
})
const downloadCount = computed(() => {
return results.value.filter(r => r.needDownload && !r.isDeleted).length
})
const deletedCount = computed(() => {
return results.value.filter(r => r.isDeleted).length
})
const activeCount = computed(() => {
return results.value.filter(r => !r.isDeleted).length
})
const filteredResults = computed(() => {
let filtered = results.value
// 按类型筛选
switch (filterType.value) {
case 'active':
filtered = filtered.filter(r => !r.isDeleted)
break
case 'included':
filtered = filtered.filter(r => r.includeInResponse && !r.isDeleted)
break
case 'download':
filtered = filtered.filter(r => r.needDownload && !r.isDeleted)
break
case 'deleted':
filtered = filtered.filter(r => r.isDeleted)
break
}
// 搜索过滤
if (searchText.value) {
const text = searchText.value.toLowerCase()
filtered = filtered.filter(r =>
r.title?.toLowerCase().includes(text) ||
r.summary?.toLowerCase().includes(text) ||
r.content?.toLowerCase().includes(text)
)
}
return filtered
})
onMounted(() => {
loadDetail()
loadResults()
})
const loadDetail = async () => {
try {
const id = route.params.id
const data = await getInquiryDetail(id)
inquiry.value = data
} catch (error) {
ElMessage.error('加载详情失败')
}
}
const loadResults = async () => {
loading.value = true
try {
const id = route.params.id
const data = await getSearchResults(id)
results.value = data || []
} catch (error) {
ElMessage.error('加载检索结果失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
loadResults()
}
const handleSelectionChange = (selection) => {
selectedResults.value = selection
}
const toggleInclude = async (row) => {
try {
const newValue = !row.includeInResponse
await updateSearchResult(row.id, { includeInResponse: newValue })
row.includeInResponse = newValue
ElMessage.success(newValue ? '已纳入回复' : '已取消纳入')
} catch (error) {
ElMessage.error('操作失败')
}
}
const toggleDownload = async (row) => {
try {
const newValue = !row.needDownload
await updateSearchResult(row.id, { needDownload: newValue })
row.needDownload = newValue
ElMessage.success(newValue ? '已标记下载' : '已取消下载')
} catch (error) {
ElMessage.error('操作失败')
}
}
const handleDelete = async (row) => {
ElMessageBox.confirm(
'确认将此条结果标记为错误并删除?',
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
await deleteSearchResult(row.id)
row.isDeleted = true
ElMessage.success('已删除')
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
const handleBatchInclude = async () => {
try {
const updates = selectedResults.value.map(r => ({
id: r.id,
includeInResponse: true
}))
await batchUpdateSearchResults(updates)
selectedResults.value.forEach(r => {
r.includeInResponse = true
})
ElMessage.success(`已将 ${updates.length} 条结果纳入回复`)
tableRef.value?.clearSelection()
} catch (error) {
ElMessage.error('批量操作失败')
}
}
const handleBatchDownload = async () => {
try {
const updates = selectedResults.value.map(r => ({
id: r.id,
needDownload: true
}))
await batchUpdateSearchResults(updates)
selectedResults.value.forEach(r => {
r.needDownload = true
})
ElMessage.success(`已将 ${updates.length} 条结果标记下载`)
tableRef.value?.clearSelection()
} catch (error) {
ElMessage.error('批量操作失败')
}
}
const handleGenerateDownloadTasks = async () => {
if (downloadSelectedIds.value.length === 0) {
ElMessage.warning('请先选择并标记需要下载的条目')
return
}
try {
await generateDownloadTasks(inquiry.value.id, downloadSelectedIds.value)
ElMessage.success('下载任务已生成,状态已更新为“下载中”')
await loadDetail()
} catch (error) {
ElMessage.error('生成下载任务失败')
}
}
const handleBatchDelete = async () => {
ElMessageBox.confirm(
`确认删除选中的 ${selectedResults.value.length} 条结果?`,
'批量删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
for (const row of selectedResults.value) {
await deleteSearchResult(row.id)
row.isDeleted = true
}
ElMessage.success('批量删除成功')
tableRef.value?.clearSelection()
} catch (error) {
ElMessage.error('批量删除失败')
}
}).catch(() => {})
}
const handleGenerateResponse = async () => {
ElMessageBox.confirm(
`将基于 ${includedCount.value} 条已纳入的资料生成回复,确认继续?`,
'生成回复',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
).then(async () => {
generating.value = true
try {
await generateResponse(inquiry.value.id)
ElMessage.success('回复生成成功,正在跳转...')
router.push(`/inquiry/${inquiry.value.id}/response-view`)
} catch (error) {
ElMessage.error('生成回复失败: ' + (error.message || '未知错误'))
} finally {
generating.value = false
}
}).catch(() => {})
}
const handleFilterChange = () => {
tableRef.value?.clearSelection()
}
const getSourceTagType = (source) => {
const typeMap = {
'内部数据': 'primary',
'知识库': 'success',
'知网': 'warning',
'ClinicalTrials.gov': 'danger',
'CNKI': 'warning'
}
return typeMap[source] || 'info'
}
const goBack = () => {
router.push(`/inquiry/${inquiry.value.id}`)
}
</script>
<style scoped>
.search-results {
width: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.batch-actions {
margin: 20px 0;
}
.batch-actions .el-button {
margin-right: 10px;
}
.filters {
display: flex;
align-items: center;
margin: 20px 0;
}
.expand-content {
padding: 20px;
}
:deep(.el-statistic__content) {
font-size: 28px;
font-weight: 600;
}
</style>