791 lines
26 KiB
Vue
791 lines
26 KiB
Vue
<template>
|
||
<div class="inquiry-detail">
|
||
<el-card v-loading="loading">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>查询详情 - {{ inquiry.requestNumber }}</span>
|
||
<el-tag :type="getStatusType(inquiry.status)">
|
||
{{ getStatusText(inquiry.status) }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
|
||
<el-descriptions :column="2" border>
|
||
<el-descriptions-item label="请求编号">
|
||
{{ inquiry.requestNumber }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="客户姓名">
|
||
{{ inquiry.customerName }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="客户邮箱">
|
||
{{ inquiry.customerEmail }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="客户职称">
|
||
{{ inquiry.customerTitle }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="指派给">
|
||
{{ inquiry.assignedTo }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="创建时间">
|
||
{{ inquiry.createdAt }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="查询内容" :span="2">
|
||
<div style="white-space: pre-wrap;">{{ inquiry.inquiryContent }}</div>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<el-divider />
|
||
|
||
<!-- 操作按钮:与“提取关键词”并排展示 -->
|
||
<div class="action-section">
|
||
<el-button
|
||
type="primary"
|
||
@click="handleExtractKeywords"
|
||
:loading="extractLoading"
|
||
:disabled="extractLoading || searchLoading || !['CREATED','SEARCHED'].includes(inquiry.status)"
|
||
>提取关键词</el-button>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleSearchWithParams"
|
||
:loading="searchLoading"
|
||
:disabled="extractLoading || searchLoading || !standardQuery?.trim()"
|
||
>一键检索</el-button>
|
||
<el-button @click="showResults" :disabled="!hasAnyResults">查看检索结果</el-button>
|
||
<el-button
|
||
type="warning"
|
||
@click="handleGenerateResponse"
|
||
:loading="processing"
|
||
:disabled="processing || inquiry.status !== 'SEARCHED'"
|
||
>生成回复</el-button>
|
||
<el-button @click="goToResponseView" :disabled="!inquiry.responseContent">查看回复</el-button>
|
||
</div>
|
||
|
||
<!-- AI识别结果 + 标准检索词 -->
|
||
<div class="result-section">
|
||
<h3>AI识别结果</h3>
|
||
<div v-if="inquiry.keywords && isStructuredKeywords(inquiry.keywords)" class="keywords-structured">
|
||
<el-descriptions :column="3" border>
|
||
<el-descriptions-item label="药物中文名">
|
||
<el-tag type="success">{{ getKeywordField(inquiry.keywords, 'drugNameChinese') }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="药物英文名">
|
||
<el-tag type="primary">{{ getKeywordField(inquiry.keywords, 'drugNameEnglish') }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="查询项目">
|
||
<el-tag type="warning">{{ getKeywordField(inquiry.keywords, 'requestItem') || '未指定' }}</el-tag>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
<div v-else>
|
||
<el-tag v-for="(keyword, index) in parseKeywords(inquiry.keywords)" :key="index" style="margin-right: 10px;">
|
||
{{ keyword }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新:检索范围与标准检索词表格(第一列包含选择框) -->
|
||
<div class="result-section">
|
||
<h3>检索范围与标准检索词</h3>
|
||
<el-table :data="tableRows" border style="width: 100%;">
|
||
<el-table-column label="检索范围" width="260">
|
||
<template #default="{ row }">
|
||
<el-checkbox
|
||
:label="row.scope"
|
||
:model-value="isScopeSelected(row.scope)"
|
||
@change="toggleScope(row.scope)"
|
||
>{{ row.label }}</el-checkbox>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="检索式(标准检索词)">
|
||
<template #default="{ row }">
|
||
<div class="query-editor">
|
||
<div
|
||
class="query-chip"
|
||
v-for="(item, idx) in scopeQueries[row.scope]"
|
||
:key="idx"
|
||
>
|
||
<el-input
|
||
v-model="scopeQueries[row.scope][idx].text"
|
||
:disabled="!isScopeSelected(row.scope) || !!item.summary"
|
||
placeholder="请输入检索式"
|
||
size="small"
|
||
style="width: 520px;"
|
||
/>
|
||
<el-button
|
||
:disabled="!isScopeSelected(row.scope)"
|
||
size="small"
|
||
type="danger"
|
||
link
|
||
@click="removeQuery(row.scope, idx)"
|
||
>删除</el-button>
|
||
</div>
|
||
<el-button
|
||
:disabled="!isScopeSelected(row.scope)"
|
||
type="primary"
|
||
size="small"
|
||
plain
|
||
style="width: 40px;"
|
||
@click="addQuery(row.scope)"
|
||
>+ 添加检索式</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结果概述" width="900">
|
||
<template #default="{ row }">
|
||
<div>
|
||
<div
|
||
v-for="(item, idx) in scopeQueries[row.scope]"
|
||
:key="`s-${row.scope}-${idx}`"
|
||
style="margin-bottom: 6px;"
|
||
>
|
||
<span v-if="item.summary">{{ item.summary }}</span>
|
||
<span v-else style="color:#909399;">—</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div style="margin-top: 8px; color:#909399; font-size: 12px;">
|
||
未选中的检索范围禁用编辑;当该条有“结果概述”表示已完成,不可修改。
|
||
</div>
|
||
</div>
|
||
|
||
<el-divider />
|
||
|
||
<!-- 结果多 Tab 展示 -->
|
||
<el-tabs v-model="activeResultsTab">
|
||
<!-- 动态:临床试验分组 Tab(按多条检索式分别展示) -->
|
||
<el-tab-pane
|
||
v-for="(group, gIdx) in clinicalTrialsGroups"
|
||
:key="`ct-${gIdx}`"
|
||
:label="group.label"
|
||
:name="group.name"
|
||
>
|
||
<!-- 临床试验部分(沿用原表格) -->
|
||
<div class="result-section">
|
||
<div class="section-header">
|
||
<h3>临床试验信息 (ClinicalTrials.gov)</h3>
|
||
<div class="section-actions">
|
||
<el-button v-if="group.data.length > 0" type="success" @click="handleExportClinicalTrials" icon="Download">
|
||
导出CSV
|
||
</el-button>
|
||
<el-button v-if="group.data.length > 0" @click="loadClinicalTrials" icon="Refresh">
|
||
刷新
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="group.data.length > 0" style="margin-top: 20px;">
|
||
<el-alert :title="`共找到 ${group.data.length} 个临床试验`" type="info" :closable="false" style="margin-bottom: 15px;" />
|
||
<el-table :data="group.data" border stripe style="width: 100%">
|
||
<el-table-column type="expand">
|
||
<template #default="{ row }">
|
||
<div style="padding: 20px;">
|
||
<el-descriptions :column="2" border>
|
||
<el-descriptions-item label="NCT ID">
|
||
<a :href="row.url" target="_blank" style="color: #409EFF;">
|
||
{{ row.nctId }}
|
||
</a>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="研究状态">
|
||
<el-tag :type="getStatusTagType(row.overallStatus)">
|
||
{{ row.overallStatus }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="研究类型">
|
||
{{ row.studyType }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="研究阶段">
|
||
{{ row.phase || 'N/A' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="开始日期">
|
||
{{ row.startDate || 'N/A' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="完成日期">
|
||
{{ row.completionDate || 'N/A' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="入组人数">
|
||
{{ row.enrollment || 'N/A' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="主要研究者">
|
||
{{ row.sponsor }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="适应症" :span="2">
|
||
<el-tag v-for="(condition, index) in row.conditions" :key="index" style="margin-right: 5px; margin-bottom: 5px;">
|
||
{{ condition }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="干预措施" :span="2">
|
||
<el-tag v-for="(intervention, index) in row.interventions" :key="index" type="success" style="margin-right: 5px; margin-bottom: 5px;">
|
||
{{ intervention }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="研究摘要" :span="2">
|
||
<div style="white-space: pre-wrap; max-height: 200px; overflow-y: auto;">
|
||
{{ row.briefSummary || 'N/A' }}
|
||
</div>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="研究地点" :span="2">
|
||
<div v-if="row.locations && row.locations.length > 0">
|
||
<div v-for="(location, index) in row.locations.slice(0, 5)" :key="index" style="margin-bottom: 5px;">
|
||
{{ location.facility }} - {{ location.city }}, {{ location.country }}
|
||
</div>
|
||
<div v-if="row.locations.length > 5" style="color: #909399;">
|
||
... 和其他 {{ row.locations.length - 5 }} 个地点
|
||
</div>
|
||
</div>
|
||
<span v-else>N/A</span>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="nctId" label="NCT ID" width="130">
|
||
<template #default="{ row }">
|
||
<a :href="row.url" target="_blank" style="color: #409EFF;">
|
||
{{ row.nctId }}
|
||
</a>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="studyTitle" label="研究标题" min-width="300" show-overflow-tooltip />
|
||
<el-table-column prop="overallStatus" label="状态" width="120">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getStatusTagType(row.overallStatus)" size="small">
|
||
{{ row.overallStatus }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="phase" label="阶段" width="100" />
|
||
<el-table-column prop="enrollment" label="入组人数" width="100" />
|
||
<el-table-column prop="sponsor" label="主要研究者" min-width="200" show-overflow-tooltip />
|
||
</el-table>
|
||
</div>
|
||
|
||
<el-empty v-else-if="!searchingClinicalTrials" description="暂无临床试验数据,请输入药品名称进行搜索" />
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="内部数据" name="INTERNAL">
|
||
<el-empty description="暂无数据或未实现" />
|
||
</el-tab-pane>
|
||
<el-tab-pane label="知识库" name="KNOWLEDGE_BASE">
|
||
<el-empty description="暂无数据或未实现" />
|
||
</el-tab-pane>
|
||
<el-tab-pane label="知网" name="CNKI">
|
||
<el-empty description="暂无数据或未实现" />
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<div class="result-section">
|
||
<el-button @click="goBack">返回列表</el-button>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, reactive } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import {
|
||
getInquiryDetail,
|
||
extractKeywords,
|
||
performSearch,
|
||
generateResponse,
|
||
reviewResponse,
|
||
completeInquiry,
|
||
searchClinicalTrials,
|
||
getClinicalTrials,
|
||
exportClinicalTrials,
|
||
performAutoSearch,
|
||
executeFullWorkflow
|
||
} from '@/api/inquiry'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
|
||
const loading = ref(false)
|
||
const processing = ref(false)
|
||
const extractLoading = ref(false)
|
||
const searchLoading = ref(false)
|
||
const inquiry = ref({})
|
||
const clinicalTrials = ref([])
|
||
// 多检索式的临床试验结果分组:[{ name, label, data }]
|
||
const clinicalTrialsGroups = ref([])
|
||
const searchingClinicalTrials = ref(false)
|
||
|
||
// 新增:标准检索词与检索范围、多Tab
|
||
const standardQuery = ref('')
|
||
const selectedSources = ref(['CLINICAL_TRIALS']) // 仍用于结果Tab默认行为
|
||
const activeResultsTab = ref('CLINICAL_TRIALS')
|
||
|
||
// 新:详情页内的范围选择控制(独立于 selectedSources 的展示用途)
|
||
const selectedScopes = ref([])
|
||
|
||
// 新:每个范围多条检索式,条目为 { text, summary }
|
||
const scopeQueries = reactive({
|
||
INTERNAL: [],
|
||
KNOWLEDGE_BASE: [],
|
||
CNKI: [],
|
||
CLINICAL_TRIALS: []
|
||
})
|
||
|
||
const scopeMeta = {
|
||
INTERNAL: { label: '内部数据' },
|
||
KNOWLEDGE_BASE: { label: '知识库' },
|
||
CNKI: { label: '知网 (CNKI)' },
|
||
CLINICAL_TRIALS: { label: 'ClinicalTrials.gov' }
|
||
}
|
||
|
||
const tableRows = computed(() => [
|
||
{ scope: 'INTERNAL', label: scopeMeta.INTERNAL.label },
|
||
{ scope: 'KNOWLEDGE_BASE', label: scopeMeta.KNOWLEDGE_BASE.label },
|
||
{ scope: 'CNKI', label: scopeMeta.CNKI.label },
|
||
{ scope: 'CLINICAL_TRIALS', label: scopeMeta.CLINICAL_TRIALS.label }
|
||
])
|
||
|
||
const isScopeSelected = (scope) => selectedScopes.value.includes(scope)
|
||
const toggleScope = (scope) => {
|
||
const idx = selectedScopes.value.indexOf(scope)
|
||
if (idx >= 0) {
|
||
selectedScopes.value.splice(idx, 1)
|
||
} else {
|
||
selectedScopes.value.push(scope)
|
||
}
|
||
}
|
||
|
||
const addQuery = (scope) => {
|
||
scopeQueries[scope].push({ text: '', summary: '' })
|
||
}
|
||
|
||
const removeQuery = (scope, index) => {
|
||
scopeQueries[scope].splice(index, 1)
|
||
}
|
||
|
||
const hasAnyResults = computed(() => {
|
||
if (clinicalTrialsGroups.value && clinicalTrialsGroups.value.length > 0) {
|
||
return clinicalTrialsGroups.value.some(g => (g.data || []).length > 0)
|
||
}
|
||
return clinicalTrials.value && clinicalTrials.value.length > 0
|
||
})
|
||
|
||
onMounted(() => {
|
||
loadDetail()
|
||
loadClinicalTrials()
|
||
})
|
||
|
||
const loadDetail = async () => {
|
||
loading.value = true
|
||
try {
|
||
const id = route.params.id
|
||
inquiry.value = await getInquiryDetail(id)
|
||
|
||
// 同步范围到 selectedScopes(初始来自后端选择)
|
||
const scopes = []
|
||
if (inquiry.value.searchInternalData) scopes.push('INTERNAL')
|
||
if (inquiry.value.searchKnowledgeBase) scopes.push('KNOWLEDGE_BASE')
|
||
if (inquiry.value.searchCnki) scopes.push('CNKI')
|
||
if (inquiry.value.searchClinicalTrials) scopes.push('CLINICAL_TRIALS')
|
||
selectedScopes.value = scopes.length ? scopes : ['CLINICAL_TRIALS']
|
||
|
||
if (inquiry.value?.keywords) {
|
||
buildStandardQueryFromKeywords()
|
||
// 由AI关键词构建每范围默认检索式(中文+事项、英文+事项)
|
||
buildDefaultQueriesFromKeywords()
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('加载详情失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 从AI关键词构建默认检索式(作为每范围多条的默认项)
|
||
const buildDefaultQueriesFromKeywords = () => {
|
||
try {
|
||
const parsed = JSON.parse(inquiry.value.keywords || '{}')
|
||
const cn = parsed.drugNameChinese || ''
|
||
const en = parsed.drugNameEnglish || ''
|
||
const item = parsed.requestItem || ''
|
||
|
||
const cnQuery = [cn, item].filter(Boolean).join(' + ')
|
||
const enQuery = [en, item].filter(Boolean).join(' + ')
|
||
|
||
const defaults = []
|
||
if (cnQuery) defaults.push({ text: cnQuery, summary: '' })
|
||
if (enQuery && enQuery !== cnQuery) defaults.push({ text: enQuery, summary: '' })
|
||
|
||
const applyIfEmpty = (scope) => {
|
||
if (!scopeQueries[scope] || scopeQueries[scope].length === 0) {
|
||
// 深拷贝每个默认检索项,避免跨范围引用同一对象导致联动
|
||
scopeQueries[scope] = defaults.map(item => ({ text: item.text, summary: item.summary }))
|
||
}
|
||
}
|
||
|
||
applyIfEmpty('CLINICAL_TRIALS')
|
||
applyIfEmpty('KNOWLEDGE_BASE')
|
||
applyIfEmpty('CNKI')
|
||
applyIfEmpty('INTERNAL')
|
||
|
||
// 维持旧按钮的行为:取 CT 第一条作为标准检索词回退
|
||
standardQuery.value = scopeQueries.CLINICAL_TRIALS[0]?.text || standardQuery.value
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
const loadClinicalTrials = async () => {
|
||
try {
|
||
const id = route.params.id
|
||
clinicalTrials.value = await getClinicalTrials(id)
|
||
// 将后端保存的单次结果映射为一个默认分组,便于统一渲染
|
||
clinicalTrialsGroups.value = [
|
||
{
|
||
name: 'CLINICAL_TRIALS',
|
||
label: 'ClinicalTrials',
|
||
data: clinicalTrials.value || []
|
||
}
|
||
]
|
||
} catch (error) {
|
||
console.error('加载临床试验数据失败', error)
|
||
}
|
||
}
|
||
|
||
const handleExtractKeywords = async () => {
|
||
extractLoading.value = true
|
||
try {
|
||
await extractKeywords(inquiry.value.id)
|
||
ElMessage.success('关键词提取成功')
|
||
await loadDetail()
|
||
buildStandardQueryFromKeywords()
|
||
buildDefaultQueriesFromKeywords()
|
||
} catch (error) {
|
||
ElMessage.error('关键词提取失败')
|
||
} finally {
|
||
extractLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleSearchWithParams = async () => {
|
||
const keyword = (standardQuery.value || '').trim()
|
||
if (!keyword) {
|
||
ElMessage.warning('请先填写标准检索词')
|
||
return
|
||
}
|
||
searchLoading.value = true
|
||
try {
|
||
// 仅当选择了 ClinicalTrials 时,执行临床试验检索
|
||
if (selectedSources.value.includes('CLINICAL_TRIALS')) {
|
||
await handleSearchClinicalTrials()
|
||
}
|
||
ElMessage.success('检索完成')
|
||
activeResultsTab.value = 'CLINICAL_TRIALS'
|
||
} catch (error) {
|
||
ElMessage.error('检索失败')
|
||
} finally {
|
||
searchLoading.value = false
|
||
}
|
||
}
|
||
|
||
const showResults = () => {
|
||
activeResultsTab.value = selectedSources.value[0] || 'CLINICAL_TRIALS'
|
||
}
|
||
|
||
const handleGenerateResponse = async () => {
|
||
processing.value = true
|
||
try {
|
||
await generateResponse(inquiry.value.id)
|
||
ElMessage.success('回复生成成功')
|
||
loadDetail()
|
||
} catch (error) {
|
||
ElMessage.error('回复生成失败')
|
||
} finally {
|
||
processing.value = false
|
||
}
|
||
}
|
||
|
||
const handleApprove = async () => {
|
||
processing.value = true
|
||
try {
|
||
await reviewResponse(inquiry.value.id, { approved: true })
|
||
ElMessage.success('回复已批准')
|
||
loadDetail()
|
||
} catch (error) {
|
||
ElMessage.error('操作失败')
|
||
} finally {
|
||
processing.value = false
|
||
}
|
||
}
|
||
|
||
const handleReject = async () => {
|
||
ElMessageBox.prompt('请输入修改意见', '要求修改', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消'
|
||
}).then(async ({ value }) => {
|
||
processing.value = true
|
||
try {
|
||
await reviewResponse(inquiry.value.id, { approved: false, comments: value })
|
||
ElMessage.success('已要求修改')
|
||
loadDetail()
|
||
} catch (error) {
|
||
ElMessage.error('操作失败')
|
||
} finally {
|
||
processing.value = false
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
const handleDownloadLiteratures = () => {
|
||
ElMessage.info('文献下载功能开发中...')
|
||
}
|
||
|
||
const handleComplete = async () => {
|
||
processing.value = true
|
||
try {
|
||
await completeInquiry(inquiry.value.id)
|
||
ElMessage.success('处理已完成')
|
||
loadDetail()
|
||
} catch (error) {
|
||
ElMessage.error('操作失败')
|
||
} finally {
|
||
processing.value = false
|
||
}
|
||
}
|
||
|
||
const goBack = () => {
|
||
router.back()
|
||
}
|
||
|
||
const getStatusType = (status) => {
|
||
const typeMap = {
|
||
'CREATED': 'info',
|
||
'SEARCHING': 'warning',
|
||
'SEARCHED': '',
|
||
'RESPONDING': 'warning',
|
||
'COMPLETED': 'success'
|
||
}
|
||
return typeMap[status] || 'info'
|
||
}
|
||
|
||
const getStatusText = (status) => {
|
||
const textMap = {
|
||
'CREATED': '已创建',
|
||
'SEARCHING': '检索中',
|
||
'SEARCHED': '已检索',
|
||
'RESPONDING': '回复中',
|
||
'COMPLETED': '已完成'
|
||
}
|
||
return textMap[status] || status
|
||
}
|
||
|
||
const parseKeywords = (keywords) => {
|
||
try {
|
||
const parsed = JSON.parse(keywords)
|
||
return Array.isArray(parsed) ? parsed : [keywords]
|
||
} catch {
|
||
return keywords ? [keywords] : []
|
||
}
|
||
}
|
||
|
||
const getStatusTagType = (status) => {
|
||
const statusMap = {
|
||
'COMPLETED': 'success',
|
||
'ACTIVE': 'primary',
|
||
'RECRUITING': 'warning',
|
||
'NOT_YET_RECRUITING': 'info',
|
||
'TERMINATED': 'danger',
|
||
'WITHDRAWN': 'danger',
|
||
'SUSPENDED': 'warning',
|
||
'ENROLLING_BY_INVITATION': 'warning'
|
||
}
|
||
return statusMap[status?.toUpperCase().replace(/ /g, '_')] || 'info'
|
||
}
|
||
|
||
const makeClinicalTabLabel = (q, idx) => {
|
||
const t = (q || '').trim()
|
||
const snippet = t.length > 20 ? t.slice(0, 20) + '…' : t
|
||
return `Clinical Trials ${idx + 1}${snippet ? ' - ' + snippet : ''}`
|
||
}
|
||
|
||
// 针对 ClinicalTrials.gov 进行检索式清洗:
|
||
// 1) 去掉加号,压缩空白;2) 若包含中文,优先回退到英文药名
|
||
const sanitizeForClinicalTrials = (q) => {
|
||
const original = (q || '').trim()
|
||
// 去掉加号、压缩空白
|
||
let cleaned = original.replace(/\+/g, ' ').replace(/\s+/g, ' ').trim()
|
||
// 若包含中文,尝试用英文药名
|
||
const hasCJK = /[\u4e00-\u9fa5]/.test(cleaned)
|
||
if (hasCJK) {
|
||
try {
|
||
const parsed = JSON.parse(inquiry.value.keywords || '{}')
|
||
const en = (parsed.drugNameEnglish || '').trim()
|
||
if (en) {
|
||
cleaned = en
|
||
}
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
return cleaned
|
||
}
|
||
|
||
const handleSearchClinicalTrials = async () => {
|
||
// 取被选中的 ClinicalTrials 范围内的所有检索式(最多两条按需求展示)
|
||
const queries = (scopeQueries.CLINICAL_TRIALS || [])
|
||
.map(q => (q.text || '').trim())
|
||
.filter(Boolean)
|
||
|
||
// 若未设置多条,则回退到标准检索词
|
||
if (queries.length === 0) {
|
||
const fallback = (standardQuery.value || '').trim()
|
||
if (fallback) queries.push(fallback)
|
||
}
|
||
|
||
if (queries.length === 0) {
|
||
ElMessage.warning('请先填写标准检索词')
|
||
return
|
||
}
|
||
|
||
searchingClinicalTrials.value = true
|
||
try {
|
||
clinicalTrialsGroups.value = []
|
||
|
||
// 逐条检索并收集结果,不依赖后端保存的数据进行展示
|
||
for (let i = 0; i < queries.length; i++) {
|
||
const q = queries[i]
|
||
const searchTerm = sanitizeForClinicalTrials(q)
|
||
if (!searchTerm) {
|
||
// 若清洗后为空,则跳过该条但仍保留一个空分组用于可见性
|
||
clinicalTrialsGroups.value.push({
|
||
name: `CLINICAL_TRIALS_${i + 1}`,
|
||
label: makeClinicalTabLabel(q, i),
|
||
data: []
|
||
})
|
||
continue
|
||
}
|
||
// 调用现有接口(该接口会清空并保存到后端),但我们直接使用返回值渲染分组,避免相互覆盖的问题
|
||
const result = await searchClinicalTrials(inquiry.value.id, searchTerm)
|
||
clinicalTrialsGroups.value.push({
|
||
name: `CLINICAL_TRIALS_${i + 1}`,
|
||
label: makeClinicalTabLabel(q, i),
|
||
data: result || []
|
||
})
|
||
}
|
||
|
||
// 默认激活第一个分组
|
||
activeResultsTab.value = clinicalTrialsGroups.value[0]?.name || 'CLINICAL_TRIALS'
|
||
ElMessage.success('临床试验搜索完成')
|
||
} catch (error) {
|
||
ElMessage.error('搜索失败: ' + (error.message || '未知错误'))
|
||
} finally {
|
||
searchingClinicalTrials.value = false
|
||
}
|
||
}
|
||
|
||
const handleExportClinicalTrials = () => {
|
||
const exportUrl = exportClinicalTrials(inquiry.value.id)
|
||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
|
||
window.open(baseURL + exportUrl, '_blank')
|
||
ElMessage.success('开始下载CSV文件')
|
||
}
|
||
|
||
const isStructuredKeywords = (keywords) => {
|
||
try {
|
||
const parsed = JSON.parse(keywords)
|
||
return parsed.drugNameChinese || parsed.drugNameEnglish || parsed.requestItem
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
const buildStandardQueryFromKeywords = () => {
|
||
try {
|
||
const parsed = JSON.parse(inquiry.value.keywords || '{}')
|
||
const parts = [parsed.drugNameChinese, parsed.drugNameEnglish, parsed.requestItem].filter(Boolean)
|
||
standardQuery.value = parts.join(' ')
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
const getKeywordField = (keywords, field) => {
|
||
try {
|
||
const parsed = JSON.parse(keywords)
|
||
return parsed[field] || ''
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
const goToResponseView = () => {
|
||
router.push(`/inquiry/${inquiry.value.id}/response-view`)
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.inquiry-detail {
|
||
width: 100%;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.action-section {
|
||
margin: 30px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.action-section .el-button {
|
||
margin: 0 10px;
|
||
}
|
||
|
||
.result-section {
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.result-section h3 {
|
||
margin-bottom: 15px;
|
||
color: #303133;
|
||
}
|
||
|
||
.result-section pre {
|
||
background-color: #f5f7fa;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.section-header h3 {
|
||
margin: 0;
|
||
}
|
||
|
||
.section-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.query-editor {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.query-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
</style>
|
||
|
||
|
||
|
||
|