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
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>
|