522 lines
15 KiB
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>
|
|
|
|
|