You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

344 lines
12 KiB

<template>
<div style="padding: 20px;">
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="节点管理" name="first">
<div style="margin-bottom: 15px; text-align: left;">
<el-button type="primary" @click="openNodeDialog(null)">新增节点</el-button>
</div>
<el-table v-loading="loading" :data="nodeData" border height="550" style="width: 100%">
<el-table-column prop="id" label="ID" width="180" show-overflow-tooltip />
<el-table-column label="名称" min-width="150">
<template #default="scope">
<span :style="{ color: scope.row.name ? 'inherit' : '#909399' }">
{{ scope.row.name || 'N/A' }}
</span>
</template>
</el-table-column>
<el-table-column label="标签" width="150">
<template #default="scope">
<el-tag size="small" v-for="label in scope.row.labels" :key="label" style="margin-right: 5px;">{{ label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="openNodeDialog(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row, 'node')">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 15px; display: flex; justify-content: flex-end;">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="nodeTotal"
:page-sizes="[10, 20, 50, 100]"
v-model:page-size="pageSize"
v-model:current-page="nodePage"
@current-change="fetchNodes"
@size-change="handleSizeChange"
/>
</div>
</el-tab-pane>
<el-tab-pane label="关系管理" name="second">
<div style="margin-bottom: 15px; text-align: left;">
<el-button type="primary" @click="openRelDialog(null)">新增关系</el-button>
</div>
<el-table v-loading="loading" :data="relData" border height="550" style="width: 100%">
<el-table-column label="起始节点" min-width="150">
<template #default="scope">
<span :style="{ color: scope.row.source ? 'inherit' : '#909399' }">
{{ scope.row.source || 'N/A' }}
</span>
</template>
</el-table-column>
<el-table-column prop="type" label="关系类型" width="180" />
<el-table-column label="结束节点" min-width="150">
<template #default="scope">
<span :style="{ color: scope.row.target ? 'inherit' : '#909399' }">
{{ scope.row.target || 'N/A' }}
</span>
</template>
</el-table-column>
<el-table-column prop="label" label="描述" width="120" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="openRelDialog(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row, 'rel')">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 15px; display: flex; justify-content: flex-end;">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="relTotal"
:page-sizes="[10, 20, 50, 100]"
v-model:page-size="pageSize"
v-model:current-page="relPage"
@current-change="fetchRels"
@size-change="handleSizeChange"
/>
</div>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="nodeDialogVisible" :title="isEdit ? '修改节点' : '新增节点'" width="420px" destroy-on-close>
<el-form :model="nodeForm" label-width="80px" @submit.prevent>
<el-form-item label="名称" required>
<el-input v-model="nodeForm.name" placeholder="请输入节点名称" clearable />
</el-form-item>
<el-form-item label="标签" required>
<el-select v-model="nodeForm.label" placeholder="请选择或输入标签" filterable allow-create style="width: 100%">
<el-option v-for="tag in dynamicLabels" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="nodeDialogVisible = false">取消</el-button>
<el-button type="primary" native-type="button" :loading="submitting" @click="submitNode">确认</el-button>
</template>
</el-dialog>
<el-dialog v-model="relDialogVisible" :title="isEdit ? '修改关系信息' : '新增关系'" width="480px" destroy-on-close>
<el-form :model="relForm" label-width="100px" @submit.prevent>
<el-form-item label="起始节点" required>
<el-autocomplete
v-model="relForm.source"
:fetch-suggestions="queryNodeSearch"
placeholder="搜索并选择起始节点"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="结束节点" required>
<el-autocomplete
v-model="relForm.target"
:fetch-suggestions="queryNodeSearch"
placeholder="搜索并选择结束节点"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="关系类型" required>
<el-select v-model="relForm.type" placeholder="关系类别" style="width: 100%">
<el-option label="不良反应 (adverseReactions)" value="adverseReactions" />
<el-option label="治疗 (treats)" value="treats" />
<el-option label="包含 (contains)" value="contains" />
<el-option label="禁忌 (contraindicated)" value="contraindicated" />
</el-select>
</el-form-item>
<el-form-item label="关系描述">
<el-input v-model="relForm.label" placeholder="例如:引起、属于、禁用于" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="relDialogVisible = false">取消</el-button>
<el-button type="primary" native-type="button" :loading="submitting" @click="submitRel">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref, onMounted, reactive} from 'vue'
import axios from 'axios'
import {ElMessage, ElMessageBox} from 'element-plus'
const BASE_URL = 'http://localhost:8088'
const pageSize = ref(20)
const activeName = ref('first')
const loading = ref(false)
const submitting = ref(false)
const nodeData = ref([]);
const nodeTotal = ref(0);
const nodePage = ref(1)
const relData = ref([]);
const relTotal = ref(0);
const relPage = ref(1)
const nodeDialogVisible = ref(false)
const relDialogVisible = ref(false)
const isEdit = ref(false)
const dynamicLabels = ref([])
const nodeForm = reactive({id: '', name: '', label: ''})
const relForm = reactive({id: '', source: '', target: '', type: 'adverseReactions', label: ''})
const handleSizeChange = (val) => {
pageSize.value = val
activeName.value === 'first' ? fetchNodes() : fetchRels()
}
const fetchAllLabels = async () => {
try {
const res = await axios.get(`${BASE_URL}/api/kg/labels`)
if (res.data.code === 200) dynamicLabels.value = res.data.data
} catch (e) {
console.error('获取标签失败:', e)
}
}
const queryNodeSearch = async (queryString, cb) => {
if (!queryString) return cb([])
try {
const res = await axios.get(`${BASE_URL}/api/kg/node/suggest`, {params: {keyword: queryString}})
if (res.data.code === 200) {
cb(res.data.data.map(name => ({value: name})))
} else cb([])
} catch (e) {
cb([])
}
}
const openNodeDialog = (row = null) => {
fetchAllLabels()
isEdit.value = !!row
if (row) {
nodeForm.id = row.id
nodeForm.name = row.name || ''
nodeForm.label = row.labels ? row.labels[0] : ''
} else {
Object.assign(nodeForm, {id: '', name: '', label: 'Drug'})
}
nodeDialogVisible.value = true
}
const openRelDialog = (row = null) => {
isEdit.value = !!row
if (row) {
Object.assign(relForm, {
id: row.id,
source: row.source || '',
target: row.target || '',
type: row.type,
label: row.label || ''
})
} else {
Object.assign(relForm, {id: '', source: '', target: '', type: 'adverseReactions', label: ''})
}
relDialogVisible.value = true
}
const fetchNodes = async () => {
loading.value = true
try {
const res = await axios.get(`${BASE_URL}/api/kg/nodes`, {params: {page: nodePage.value, pageSize: pageSize.value}})
if (res.data.code === 200) {
nodeData.value = res.data.data.items
nodeTotal.value = res.data.data.total
}
} finally {
loading.value = false
}
}
const fetchRels = async () => {
loading.value = true
relData.value = []
try {
const res = await axios.get(`${BASE_URL}/api/kg/relationships`, {
params: { page: relPage.value, pageSize: pageSize.value }
})
if (res.data.code === 200) {
relData.value = res.data.data.items
relTotal.value = res.data.data.total
}
} finally {
loading.value = false
}
}
const submitNode = async () => {
if (!nodeForm.name || !nodeForm.name.trim()) return ElMessage.warning('名称不能为空')
submitting.value = true
const url = isEdit.value ? `${BASE_URL}/api/kg/node/update` : `${BASE_URL}/api/kg/node/add`
const payload = {
name: nodeForm.name.trim(),
label: nodeForm.label
}
if (isEdit.value) payload.id = nodeForm.id
try {
const res = await axios.post(url, payload)
if (res.data.code === 200) {
ElMessage.success(res.data.msg || '操作成功')
nodeDialogVisible.value = false
fetchNodes()
} else {
ElMessage.error(res.data.msg || '操作失败')
}
} catch (e) {
ElMessage.error('网络连接错误')
} finally {
submitting.value = false
}
}
const submitRel = async () => {
if (!relForm.source || !relForm.target) return ElMessage.warning('节点信息不完整')
submitting.value = true
const url = isEdit.value ? `${BASE_URL}/api/kg/rel/update` : `${BASE_URL}/api/kg/rel/add`
const payload = {
source: String(relForm.source).trim(),
target: String(relForm.target).trim(),
type: relForm.type,
label: (relForm.label || '').trim()
}
if (isEdit.value) {
payload.id = relForm.id
}
try {
const res = await axios.post(url, payload)
if (res.data.code === 200) {
relDialogVisible.value = false
ElMessage.success(res.data.msg || '关系已同步')
fetchRels()
} else {
ElMessage.warning(res.data.msg || '操作受限')
}
} catch (e) {
ElMessage.error('系统响应异常')
} finally {
submitting.value = false
}
}
const handleDelete = (row, type) => {
const isNode = type === 'node'
const displayName = row.name || '未命名节点'
ElMessageBox.confirm(
isNode ? `删除节点 "${displayName}" 会同步删除所有关联关系,确认吗?` : '确认删除该关系吗?',
'风险提示', {type: 'error', confirmButtonText: '确定删除', cancelButtonText: '取消'}
).then(async () => {
try {
const res = await axios.post(`${BASE_URL}/api/kg/${isNode ? 'node' : 'rel'}/delete`, {id: row.id})
if (res.data.code === 200) {
ElMessage.success('已从数据库移除')
isNode ? (fetchNodes(), fetchRels()) : fetchRels()
}
} catch (e) {
ElMessage.error('操作执行失败')
}
}).catch(() => {
})
}
const handleTabClick = (pane) => {
pane.paneName === 'first' ? fetchNodes() : fetchRels()
}
onMounted(() => {
fetchNodes()
fetchAllLabels()
})
</script>