Browse Source

Merge branch 'hanyuqing' of http://124.70.32.114:3100/hanyuqing/KGPython into mh

# Conflicts:
#	vue/src/system/GraphDemo.vue
#	vue/src/system/GraphStyle.vue
mh
hanyuqing 3 months ago
parent
commit
0a1c63cef3
  1. 2
      vue/src/system/GraphBuilder.vue
  2. 264
      vue/src/system/GraphDemo.vue
  3. 484
      vue/src/system/GraphQA.vue
  4. 30
      vue/src/system/GraphStyle.vue

2
vue/src/system/GraphBuilder.vue

@ -335,11 +335,13 @@ export default {
allSelect(msg){
if (msg.entities && msg.entities.length > 0) {
msg.entities.forEach(ent => {
ent.selected=true
this.selectedEntities.push(ent);
});
}
if (msg.relations && msg.relations.length > 0) {
msg.relations.forEach(rel => {
rel.selected=true
this.selectedRelations.push(rel);
});
}

264
vue/src/system/GraphDemo.vue

@ -134,13 +134,6 @@
</div>
</div>
<div class="graph-viewport">
<GraphToolbar
ref="toolbarRef"
v-if="_graph"
:graph="_graph"
class="toolbar-position"
style="position: absolute; top: 40px; left: 750px; z-index: 1000; width: auto;"
/>
<div ref="graphContainer" class="graph-container" id="container"></div>
</div>
</section>
@ -162,18 +155,14 @@ import {
import {Graph, Tooltip} from '@antv/g6';
import Menu from "@/components/Menu.vue";
import {a} from "vue-router/dist/devtools-EWN81iOl.mjs";
import GraphToolbar from "@/components/GraphToolbar.vue";
import Fuse from 'fuse.js';
import {ElMessage} from "element-plus";
import {getGraphStyleActive} from "@/api/style";
export default {
name: 'Display',
components: {Menu, GraphToolbar},
components: {Menu},
data() {
return {
_graph: null,
G6: null, //
//
nodeShowLabel: true,
@ -212,41 +201,41 @@ export default {
children: 'children',
label: 'title' // el-tree
},
typeRadio: "Disease",
DiseaseRadio: "ICD10",
drugTree: [],
diseaseDepartTree: [],
diseaseICD10Tree: [],
diseaseSZMTree: [],
checkTree: [],
typeRadio:"Disease",
DiseaseRadio:"ICD10",
drugTree:[],
diseaseDepartTree:[],
diseaseICD10Tree:[],
diseaseSZMTree:[],
checkTree:[],
legendItems: [
{key: 'Disease', label: '疾病', color: '#EF4444'},
{key: 'Drug', label: '药品', color: '#91cc75'},
{key: 'Check', label: '检查', color: '#336eee'},
{key: 'Symptom', label: '症状', color: '#fac858'},
{key: 'Other', label: '其他', color: '#59d1d4'}
{ key: 'Disease', label: '疾病', color: '#EF4444' },
{ key: 'Drug', label: '药品', color: '#91cc75' },
{ key: 'Check', label: '检查', color: '#336eee' },
{ key: 'Symptom', label: '症状', color: '#fac858' },
{ key: 'Other', label: '其他', color: '#59d1d4' }
],
visibleCategories: new Set(), //
diseaseCount: 0,
drugCount: 0,
checkCount: 0,
diseaseCount:0,
drugCount:0,
checkCount:0,
originalNodeStyles: new Map(), //
originalEdgeStyles: new Map(), //
searchResults: {
nodes: [],
edges: []
},
searchKeyword: "",
drugSubjectTree: [],
DrugRadio: "Subject",
searchKeyword:"",
drugSubjectTree:[],
DrugRadio:"Subject",
isDropdownOpen: false,
typeOptions: [
{value: 'Disease', label: '疾病'},
{value: 'Drug', label: '药品'},
{value: 'Check', label: '检查'}
{ value: 'Disease', label: '疾病' },
{ value: 'Drug', label: '药品' },
{ value: 'Check', label: '检查' }
],
configs: [],
parsedStyles: {},
configs:[],
parsedStyles:{},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
@ -325,7 +314,7 @@ export default {
this.loadDrugTreeData()
this.loadCheckTreeData()
this.loadDrugSubjectTreeData()
this.treeData = this.diseaseICD10Tree
this.treeData=this.diseaseICD10Tree
await this.$nextTick();
try {
await this.getDefault()
@ -367,7 +356,7 @@ export default {
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType || this.edgeType,
type: styleConf.edgeType ||this.edgeType,
style: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
@ -443,7 +432,7 @@ export default {
},
beforeUnmount() {
if (this._graph != null) {
if (this._graph!=null){
this._graph.stopLayout();
this.clearGraphState();
this._graph.destroy()
@ -482,7 +471,7 @@ export default {
return {};
}
},
async getDefault() {
async getDefault(){
const response = await getGraphStyleActive();
const data = response.data;
if (!Array.isArray(data) || data.length === 0) {
@ -650,7 +639,7 @@ export default {
// 4
const firstMatch = nodeIds[0] || edgeIds[0];
if (firstMatch && (this._graph.hasNode(firstMatch) || this._graph.hasEdge(firstMatch))) {
this._graph.focusElement(firstMatch, {animation: true, duration: 600});
this._graph.focusElement(firstMatch, { animation: true, duration: 600 });
}
},
clearSearchHighlight() {
@ -664,26 +653,26 @@ export default {
this._graph.setElementState(id, 'normal', true);
});
},
getCount() {
getCount().then(res => {
this.diseaseCount = res.Disease
this.drugCount = res.Drug
this.checkCount = res.Check
getCount(){
getCount().then(res=>{
this.diseaseCount=res.Disease
this.drugCount=res.Drug
this.checkCount=res.Check
console.log(res)
})
},
buildCategoryIndex() {
const index = {};
if (this._graph != null) {
if (this._graph!=null){
const nodes = this._graph.getNodeData() //
nodes.forEach(node => {
console.log(node.data.label)
const category = node.data.label; // label
if (category == 'Drug' || category == 'Symptom' ||
category == 'Disease' || category == 'Check') {
if(category=='Drug'||category=='Symptom'||
category=='Disease'||category=='Check'){
if (!index[category]) index[category] = [];
index[category].push(node.id);
} else {
}else{
if (!index["Other"]) index["Other"] = [];
index["Other"].push(node.id);
}
@ -693,7 +682,7 @@ export default {
},
//
toggleCategory(key) {
toggleCategory (key){
if (this.visibleCategories.has(key)) {
this.visibleCategories.delete(key)
} else {
@ -748,28 +737,28 @@ export default {
}
},
changeTree() {
if (this.typeRadio == "Disease") {
if (this.DiseaseRadio == "ICD10") {
this.treeData = this.diseaseICD10Tree
changeTree(){
if(this.typeRadio=="Disease"){
if(this.DiseaseRadio=="ICD10") {
this.treeData=this.diseaseICD10Tree
}
if (this.DiseaseRadio == "Department") {
this.treeData = this.diseaseDepartTree
if(this.DiseaseRadio=="Department") {
this.treeData=this.diseaseDepartTree
}
if (this.DiseaseRadio == "SZM") {
this.treeData = this.diseaseSZMTree
if(this.DiseaseRadio=="SZM") {
this.treeData=this.diseaseSZMTree
}
}
if (this.typeRadio == "Drug") {
if (this.DrugRadio == "Subject") {
this.treeData = this.drugSubjectTree
if(this.typeRadio=="Drug") {
if(this.DrugRadio=="Subject") {
this.treeData=this.drugSubjectTree
}
if (this.DrugRadio == "SZM") {
this.treeData = this.drugTree
if(this.DrugRadio=="SZM") {
this.treeData=this.drugTree
}
}
if (this.typeRadio == "Check") {
this.treeData = this.checkTree
if(this.typeRadio=="Check") {
this.treeData=this.checkTree
}
},
async loadDiseaseICD10TreeData() {
@ -829,15 +818,15 @@ export default {
const response = await getGraph(data); // Promise
this.formatData(response)
}
if (data.level == "category" ||
data.level == "subcategory" ||
data.level == "diagnosis") {
data.type = "Disease"
if(data.level=="category"||
data.level=="subcategory"||
data.level=="diagnosis"){
data.type="Disease"
const response = await getGraph(data); // Promise
this.formatData(response)
}
if (data.type === "Disease") {
data.type = "Disease"
if(data.type === "Disease"){
data.type="Disease"
const response = await getGraph(data); // Promise
this.formatData(response)
}
@ -853,17 +842,17 @@ export default {
// 1.
this._graph.getNodeData().forEach(node => {
this._graph.setElementState(node.id, []);
this._graph.setElementState(node.id,[]);
});
this._graph.getEdgeData().forEach(edge => {
this._graph.setElementState(edge.id, []);
this._graph.setElementState(edge.id,[]);
});
// 2. pending
// clearTimeout
},
formatData(data) {
formatData(data){
this._graph.stopLayout();
this.clearGraphState();
@ -904,7 +893,7 @@ export default {
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType || this.edgeType,
type: styleConf.edgeType ||this.edgeType,
style: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
@ -950,7 +939,7 @@ export default {
},
initGraph() {
if (this._graph != null) {
if (this._graph!=null){
this._graph.destroy()
this._graph = null;
}
@ -961,7 +950,7 @@ export default {
console.log(this._nodeLabelMap)
const container = this.$refs.graphContainer;
if (!container) return;
if (container != null) {
if (container!=null){
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
console.log(width)
@ -1001,8 +990,8 @@ export default {
easing: 'ease-in-out', //
},
},
behaviors: ['zoom-canvas', 'drag-element',
'click-select', 'focus-element', {
behaviors: [ 'zoom-canvas', 'drag-element',
'click-select','focus-element', {
type: 'hover-activate',
degree: 1,
},
@ -1049,7 +1038,7 @@ export default {
shadowBlur: 10,
opacity: 1
},
highlight: {
highlight:{
stroke: '#FF5722',
lineWidth: 4,
opacity: 1
@ -1057,7 +1046,7 @@ export default {
inactive: {
opacity: 0.8
},
normal: {
normal:{
opacity: 1
}
@ -1105,14 +1094,14 @@ export default {
inactive: {
opacity: 0.8
},
normal: {
normal:{
opacity: 1
}
},
},
data: this.defaultData,
data:this.defaultData,
});
this.$nextTick(() => {
@ -1170,23 +1159,23 @@ export default {
// });
graph.on('node:click', (evt) => {
const nodeItem = evt.target.id; //
let node = graph.getNodeData(nodeItem).data
let data = {
label: node.name,
type: node.label
let node=graph.getNodeData(nodeItem).data
let data={
label:node.name,
type:node.label
}
getGraph(data).then(response => {
getGraph(data).then(response=>{
console.log(response)
this.formatData(response)
}); // Promise
});
graph.once('afterlayout', () => {
if (!graph.destroyed) {
graph.fitCenter({padding: 40, duration: 1000});
graph.fitCenter({ padding: 40, duration: 1000 });
}
});
this._graph = graph
this._graph.setPlugins([{
this._graph.setPlugins([ {
type: 'tooltip',
// tooltip
enable: (e) => e.targetType === 'edge',
@ -1195,7 +1184,7 @@ export default {
console.log(e)
const edge = items[0]; //
if (!edge) return '';
const data = items[0].data
const data=items[0].data
const sourceId = edge.source;
const targetId = edge.target;
@ -1212,67 +1201,10 @@ export default {
${sourceName} ${rel} > ${targetName}
</div>`;
},
},
{
type: 'toolbar',
onClick: (id) => {
if (id === 'reset') {
// 1.
this.localResetGraph();
} else if (this.$refs.toolbarRef) {
// 2.
this.$refs.toolbarRef.handleToolbarAction(id);
}
},
getItems: () => {
return [
{id: 'zoom-in', value: 'zoom-in', title: '放大'},
{id: 'zoom-out', value: 'zoom-out', title: '缩小'},
{id: 'auto-fit', value: 'auto-fit', title: '聚焦'},
{id: 'reset', value: 'reset', title: '重置'},
{id: 'export', value: 'export', title: '导出图谱'},
];
},
},
])
}
},
localResetGraph() {
// 1.
if (!this._graph) return;
if (!this.defaultData || !this.defaultData.nodes) {
this.$message.warning("未找到可重置的数据源");
return;
}
try {
// 2.
const canvas = this._graph.getCanvas();
if (canvas && typeof canvas.setCursor === 'function') {
canvas.setCursor('default');
},])
}
// 3.
// DOM EventBoundary
this._graph.destroy();
this._graph = null;
// 4.
this.$nextTick(() => {
// initGraph this.defaultData
this.initGraph();
// 5. destroy
this.buildCategoryIndex();
this.$message.success("图谱已重置");
});
} catch (err) {
console.error('重置图谱失败:', err);
//
// location.reload();
}
},
updateGraph(data) {
@ -1286,7 +1218,7 @@ export default {
const updatedNodes = this.defaultData.nodes.map(node => ({
...node,
type: this.nodeShape,
style: {
style:{
size: this.nodeSize,
fill: this.nodeFill,
stroke: this.nodeStroke,
@ -1309,7 +1241,7 @@ export default {
if (!this._graph) return
const updatedEdges = this.defaultData.edges.map(edge => ({
...edge,
type: this.edgeType,
type:this.edgeType,
style: {
endArrow: this.edgeEndArrow,
stroke: this.edgeStroke,
@ -1544,7 +1476,6 @@ button:hover {
width: 16px;
}
/*.search-btn:hover {
background-color: rgba(34, 101, 244, 0.64);
border-radius: 50%;
@ -1593,14 +1524,12 @@ button:hover {
padding: 0 2px 0px 15px;
justify-content: space-between;
}
.d-title {
.d-title{
display: flex;
align-items: center;
font-size: 13px;
}
.d-count {
.d-count{
font-size: 9px;
background-color: #5989F0;
border-radius: 7px;
@ -1647,13 +1576,11 @@ button:hover {
text-align: left;
font-size: 12px;
}
.disease-body {
.disease-body{
width: 360px;
overflow: scroll;
height: 74vh;
}
/* 隐藏滚动条,但允许滚动 */
.disease-body {
/* Firefox */
@ -1679,12 +1606,12 @@ button:hover {
/deep/ .radio-drug .el-radio__input.is-checked .el-radio__inner {
background: #52c41a; /* 检查的颜色 */
border-color: #52c41a;
border-color:#52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked .el-radio__inner {
background: #1890ff; /* 药品的颜色 */
border-color: #1890ff;
border-color:#1890ff;
}
@ -1697,28 +1624,27 @@ button:hover {
/deep/ .radio-drug .el-radio__input.is-checked .el-radio__inner:hover {
background: #52c41a; /* 检查的颜色 */
border-color: #52c41a;
border-color:#52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked .el-radio__inner:hover {
background: #1890ff; /* 药品的颜色 */
border-color: #1890ff;
border-color:#1890ff;
}
/* 自定义选中后的样式 */
/deep/ .radio-disease .el-radio__input.is-checked + .el-radio__label {
/deep/ .radio-disease .el-radio__input.is-checked+.el-radio__label {
color: rgb(153, 10, 0);
}
/deep/ .radio-drug .el-radio__input.is-checked + .el-radio__label {
/deep/ .radio-drug .el-radio__input.is-checked+.el-radio__label {
color: #52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked + .el-radio__label {
/deep/ .radio-check .el-radio__input.is-checked+.el-radio__label {
color: #1890ff;
}
/* 自定义下拉样式 */
.select-container {
position: relative;
@ -1748,7 +1674,7 @@ button:hover {
background: rgba(255, 255, 255, 0.64);
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10;
min-width: 80px;
color: #000;

484
vue/src/system/GraphQA.vue

@ -33,7 +33,7 @@
<div class="answer-item" style=" background-color: #165DFF;
border-color: #0066cc;
color: #fff; height: auto;max-height: 3vw">
{{queryRecord}}
{{queryRecord}}
</div>
</div>
<h2 class="section-title">问答结果</h2>
@ -63,12 +63,6 @@
<!-- 右侧知识图谱 -->
<div class="knowledge-graph">
<GraphToolbar
v-if="_graph"
:graph="_graph"
ref="toolbarRef"
/>
<div ref="graphContainer" class="graph-container" id="container"></div>
</div>
</div>
@ -79,21 +73,21 @@
<script>
import Menu from "@/components/Menu.vue";
import { qaAnalyze } from "@/api/qa";
import { Graph } from "@antv/g6";
import { getGraphStyleActive } from "@/api/style";
import GraphToolbar from '@/components/GraphToolbar.vue';
import {qaAnalyze} from "@/api/qa";
import {Graph} from "@antv/g6";
import {getGraph} from "@/api/graph";
import {getGraphStyleActive} from "@/api/style";
export default {
name: 'GraghQA',
components: { Menu, GraphToolbar },
components: {Menu},
data() {
return {
_graph: null,
query: "",
answers: [],
selected: 0,
//
query:"",
answers:[],
selected:0,
//
nodeShowLabel: true,
nodeFontSize: 12,
nodeFontColor: '#fff',
@ -104,7 +98,7 @@ export default {
nodeLineWidth: 2,
nodeFontFamily: 'Microsoft YaHei, sans-serif',
//
//
edgeShowLabel: true,
edgeFontSize: 10,
edgeFontColor: '#666666',
@ -114,10 +108,11 @@ export default {
edgeEndArrow: true,
edgeFontFamily: 'Microsoft YaHei, sans-serif',
queryRecord: "",
isSending: false,
configs: [],
parsedStyles: {},
queryRecord:"",
isSending:false,
configs:[],
parsedStyles:{},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
@ -125,31 +120,41 @@ export default {
Symptom: '症状',
Other: '其他'
}
};
},
// =============== 👇 ===============
beforeRouteLeave(to, from, next) {
this.saveDataToLocalStorage();
next();
next(); //
},
// =======================================================================
async mounted() {
await this.getDefault();
await this.getDefault()
// =============== 👇 localStorage ===============
this.restoreDataFromLocalStorage();
// =======================================================================
// this.answers=[]
//
if (this.answers.length > 0) {
this.initGraph(this.answers[this.selected].result);
this.initGraph(this.answers[0].result);
// console.log(this.answers[0].result)
}
},
beforeUnmount() {
// =============== 👇==============
this.saveDataToLocalStorage();
// beforeunload
window.removeEventListener('beforeunload', this.handleBeforeUnload);
// =======================================================================
},
created() {
// =============== 👇/ ===============
window.addEventListener('beforeunload', this.handleBeforeUnload);
// =======================================================================
},
methods: {
safeParseStyles(stylesStr) {
try {
@ -159,8 +164,7 @@ export default {
return {};
}
},
async getDefault() {
async getDefault(){
const response = await getGraphStyleActive();
const data = response.data;
if (!Array.isArray(data) || data.length === 0) {
@ -168,23 +172,27 @@ export default {
this.parsedStyles = {};
return;
}
// is_active=1
const activeGroup = data[0];
this.configs = Array.isArray(activeGroup.configs) ? activeGroup.configs : [];
// label -> style
const styleMap = {};
this.configs.forEach(config => {
const label = config.current_label;
styleMap[label] = this.safeParseStyles(config.styles);
});
this.parsedStyles = styleMap;
console.log(this.parsedStyles)
},
buildNodeLabelMap(nodes) {
this._nodeLabelMap = new Map();
nodes.forEach(node => {
this._nodeLabelMap.set(node.id, node.data?.type || 'default');
});
},
// =============== 👇 ===============
saveDataToLocalStorage() {
try {
localStorage.setItem('graphQA_queryRecord', this.queryRecord);
@ -194,110 +202,89 @@ export default {
console.warn('⚠️ 无法保存到 localStorage:', e);
}
},
// =======================================================================
// =============== 👇 ===============
restoreDataFromLocalStorage() {
try {
const savedQuery = localStorage.getItem('graphQA_queryRecord');
const savedAnswers = localStorage.getItem('graphQA_answers');
if (savedQuery !== null) this.queryRecord = savedQuery;
if (savedQuery !== null) {
this.queryRecord = savedQuery;
}
if (savedAnswers !== null) {
this.answers = JSON.parse(savedAnswers);
// selected
if (this.answers.length > 0) {
this.selected = Math.min(this.selected, this.answers.length - 1);
}
}
console.log('✅ 数据已从 localStorage 恢复');
} catch (e) {
console.warn('⚠️ 无法从 localStorage 恢复数据:', e);
//
localStorage.removeItem('graphQA_queryRecord');
localStorage.removeItem('graphQA_answers');
}
},
// =======================================================================
// =============== 👇/ ===============
handleBeforeUnload(event) {
this.saveDataToLocalStorage();
//
event.preventDefault();
event.returnValue = '';
event.returnValue = ''; //
},
selectGraph(index) {
this.selected = index;
if (this.answers.length > 0) {
this.formatData(this.answers[index].result);
selectGraph(index){
this.selected=index
if(this.answers.length>0){
this.formatData(this.answers[index].result)
}
},
handleSearch() {
this.isSending = true;
this.answers = [];
if (this._graph) this._graph.clear();
let data = { text: this.query };
this.queryRecord = this.query;
this.query = "";
qaAnalyze(data).then(res => {
this.answers = res;
if (this.answers && this.answers.length > 0) {
this.initGraph(this.answers[0].result);
}
this.isSending = false;
}).catch(err => {
console.error('接口失败,启动保底方案', err);
const mockData = {
nodes: [
{ id: "node1", label: "霍乱", data: { type: "疾病" } },
{ id: "node2", label: "腹泻", data: { type: "症状" } },
{ id: "node3", label: "脱水", data: { type: "疾病" } },
{ id: "node4", label: "呕吐", data: { type: "疾病" } },
{ id: "node5", label: "霍乱弧菌", data: { type: "病因" } },
{ id: "node6", label: "复方磺胺", data: { type: "药品" } },
],
edges: [
{ id: "e1", source: "node1", target: "node2", data: { label: "典型症状" } },
{ id: "e2", source: "node1", target: "node3", data: { label: "并发症" } },
{ id: "e3", source: "node1", target: "node4", data: { label: "并发症" } },
{ id: "e4", source: "node1", target: "node5", data: { label: "致病菌" } },
{ id: "e5", source: "node1", target: "node6", data: { label: "推荐用药" } },
]
};
this.answers = [{ answer: "连接失败,显示预览版。", result: mockData }];
this.initGraph(this.answers[0].result);
this.isSending = false;
});
},
formatData(data) {
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' };
const getStandardLabel = (rawType) => typeMap[rawType] || rawType;
const nodeIdToData = {};
handleSearch(){
this.isSending=true
this.answers=[]
if (this._graph){
this._graph.clear()
}
let data={
text:this.query
}
this.queryRecord=this.query
this.query=""
// this.answers=[{"answer":"尿",
// "result":{"nodes":[{"id":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","label":"尿","type":""},{"id":"89f9498b-7a83-4361-889b-f96dfdfb802c","label":"","type":""},{"id":"03201620-ba9f-4957-b7d3-f89c5a115e37","label":"","type":""},{"id":"b0bace0a-eedc-485c-90d3-0a5378dc5556","label":"","type":""},{"id":"ffc12d7b-60e5-4ffa-a945-a0769f6e1047","label":"","type":""},{"id":"a7e94ee7-072b-456f-bc0c-354545851c38","label":"","type":""}],"edges":[{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"03201620-ba9f-4957-b7d3-f89c5a115e37","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"b0bace0a-eedc-485c-90d3-0a5378dc5556","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"ffc12d7b-60e5-4ffa-a945-a0769f6e1047","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"a7e94ee7-072b-456f-bc0c-354545851c38","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"89f9498b-7a83-4361-889b-f96dfdfb802c","label":""}]}}]
// this.initGraph(this.answers[0].result)
// this.formatData(this.answers[0].result)
qaAnalyze(data).then(res=>{
this.answers=res
if(this.answers.length>0){
this.initGraph(this.answers[0].result)
}
this.isSending=false
})
},
formatData(data){
// this._graph.stopLayout();
// this.clearGraphState();
// === 1. nodeId label ===
const nodeIdToEnLabel = {};
data.nodes.forEach(node => {
nodeIdToData[node.id] = {
enLabel: getStandardLabel(node.data.type),
rawType: node.data.type
};
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease"
});
// === 2. label ===
const updatedNodes = data.nodes.map(node => {
const enLabel = getStandardLabel(node.data.type);
const enLabel = node.data.type;
const styleConf = this.parsedStyles[enLabel] || {};
// 💡 styleConf
let fColor = styleConf.nodeFill;
if (!fColor) {
if (node.data.type === '疾病') fColor = '#EF4444';
else if (node.data.type === '药品' || node.data.type === '药物') fColor = '#91cc75';
else if (node.data.type === '症状') fColor = '#fac858';
else if (node.data.type === '检查') fColor = '#336eee';
else fColor = this.nodeFill;
}
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
data: { ...node.data, label: enLabel, name: node.label },
style: {
...node.style,
size: styleConf.nodeSize || this.nodeSize,
fill: fColor,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
@ -308,97 +295,64 @@ export default {
};
});
// === 3. source label ===
const updatedEdges = data.edges.map(edge => {
const sourceInfo = nodeIdToData[edge.source];
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {};
// 💡
let eStroke = styleConf.edgeStroke;
if (!eStroke) {
if (sourceInfo.rawType === '疾病') eStroke = 'rgba(239, 68, 68, 0.4)';
else if (sourceInfo.rawType === '药品' || sourceInfo.rawType === '药物') eStroke = 'rgba(145, 204, 117, 0.4)';
else if (sourceInfo.rawType === '症状') eStroke = 'rgba(250, 200, 88, 0.4)';
else eStroke = this.edgeStroke;
}
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const styleConf = this.parsedStyles[sourceEnLabel] || {};
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType || this.edgeType,
type: styleConf.edgeType ||this.edgeType,
style: {
...edge.style,
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: eStroke,
stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: styleConf.edgeFontColor || this.edgeFontColor
},
data: { ...edge.data, label: edge.data?.label || "" },
}
};
});
const pureData = JSON.parse(JSON.stringify({ nodes: updatedNodes, edges: updatedEdges }));
this.updateGraph(pureData);
},
// === 4. ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
};
updateGraph(data) {
if (!this._graph) return;
this._graph.setData(data);
this._graph.render();
this.updateGraph(updatedData)
},
updateGraph(data) {
if (!this._graph) return
localResetGraph() {
if (!this._graph) return;
const currentResult = this.answers[this.selected]?.result;
if (!currentResult) return;
this._graph.destroy();
this._graph = null;
this.$nextTick(() => {
this.initGraph(currentResult);
this.$message.success("图谱已重置");
});
this._graph.setData(data)
this._graph.render()
},
initGraph(data) {
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' };
const getStandardLabel = (rawType) => typeMap[rawType] || rawType;
if (this._graph != null) {
this._graph.destroy();
if (this._graph!=null){
this._graph.destroy()
this._graph = null;
}
const nodeIdToData = {};
console.log(data)
// === 1. nodeId label ===
const nodeIdToEnLabel = {};
data.nodes.forEach(node => {
nodeIdToData[node.id] = {
enLabel: getStandardLabel(node.data.type),
rawType: node.data.type
};
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease"
});
console.log(nodeIdToEnLabel)
// === 2. label ===
const updatedNodes = data.nodes.map(node => {
const enLabel = getStandardLabel(node.data.type);
const enLabel = node.data.type;
const styleConf = this.parsedStyles[enLabel] || {};
let fColor = styleConf.nodeFill;
if (!fColor) {
if (node.data.type === '疾病') fColor = '#EF4444';
else if (node.data.type === '药品' || node.data.type === '药物') fColor = '#91cc75';
else if (node.data.type === '症状') fColor = '#fac858';
else if (node.data.type === '检查') fColor = '#336eee';
else fColor = this.nodeFill;
}
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
data: { ...node.data, label: enLabel, name: node.label },
style: {
...node.style,
size: styleConf.nodeSize || this.nodeSize,
fill: fColor,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
@ -409,27 +363,19 @@ export default {
};
});
// === 3. source label ===
const updatedEdges = data.edges.map(edge => {
const sourceInfo = nodeIdToData[edge.source];
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {};
let eStroke = styleConf.edgeStroke;
if (!eStroke) {
if (sourceInfo.rawType === '疾病') eStroke = 'rgba(239, 68, 68, 0.4)';
else if (sourceInfo.rawType === '药品' || sourceInfo.rawType === '药物') eStroke = 'rgba(145, 204, 117, 0.4)';
else if (sourceInfo.rawType === '症状') eStroke = 'rgba(250, 200, 88, 0.4)';
else eStroke = this.edgeStroke;
}
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const styleConf = this.parsedStyles[sourceEnLabel] || {};
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType || this.edgeType,
data: { ...edge.data, label: edge.data?.label || "" },
type: styleConf.edgeType ||this.edgeType,
style: {
...edge.style,
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: eStroke,
stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
@ -439,68 +385,65 @@ export default {
};
});
const finalData = JSON.parse(JSON.stringify({
// === 4. ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
}));
this.buildNodeLabelMap(finalData.nodes);
};
this.buildNodeLabelMap(updatedNodes);
const container = this.$refs.graphContainer;
if (container != null) {
console.log(container)
if (container!=null){
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
console.log(width)
console.log(height)
const graph = new Graph({
container,
width,
height,
plugins: [
{
type: 'toolbar',
key: 'g6-toolbar',
onClick: (id) => {
if (id === 'reset') this.localResetGraph();
else if (this.$refs.toolbarRef) this.$refs.toolbarRef.handleToolbarAction(id);
},
getItems: () => [
{ id: 'zoom-in', value: 'zoom-in', title: '放大' },
{ id: 'zoom-out', value: 'zoom-out', title: '缩小' },
{ id: 'undo', value: 'undo', title: '撤销' },
{ id: 'redo', value: 'redo', title: '重做' },
{ id: 'auto-fit', value: 'auto-fit', title: '聚焦' },
{ id: 'reset', value: 'reset', title: '重置' },
{ id: 'export', value: 'export', title: '导出图谱' },
],
},
{ type: 'history', key: 'history' },
],
layout: {
type: 'force',
gravity: 0.3,
repulsion: 500,
attraction: 20,
preventOverlap: true
},
behaviors: [
'zoom-canvas', 'drag-element', 'click-select', 'focus-element',
{
type: 'force', //
gravity: 0.3, //
repulsion: 500, //
attraction: 20, //
preventOverlap: true //
},
behaviors: [ 'zoom-canvas', 'drag-element',
'click-select','focus-element', {
type: 'hover-activate',
degree: 1,
enable: (e) => e.target && e.target.id && e.action !== 'drag'
},
{
type: 'drag-canvas',
enable: (event) => event.shiftKey === false,
},
{ type: 'brush-select' },
{
type: 'brush-select',
},
],
node: {
style: {
fill: (d) => d.style?.fill,
stroke: (d) => d.style?.stroke,
size: (d) => d.style?.size,
lineWidth: (d) => d.style?.lineWidth,
// fill: (d) => {
//
// const label = d.data?.type;
// if (label === '') return '#EF4444'; //
// if (label === ''||label === '') return '#91cc75'; // 绿
// if (label === '') return '#fac858'; //
// if (label === '') return '#336eee'; //
// return '#59d1d4'; //
// },
// stroke: (d) => {
// const label = d.data?.type;
// if (label === '') return '#B91C1C';
// if (label === ''||label === '') return '#047857';
// if (label === '') return '#1D4ED8'; //
// if (label === '') return '#B45309';
// return '#40999b';
// },
labelText: (d) => d.label,
labelPlacement: 'center',
labelWordWrap: true,
@ -508,37 +451,55 @@ export default {
labelMaxLines: 3,
labelTextOverflow: 'ellipsis',
labelTextAlign: 'center',
labelFill: (d) => d.style?.labelFill,
labelFontSize: (d) => d.style?.labelFontSize,
labelFontFamily: (d) => d.style?.labelFontFamily,
opacity: 1
},
state: {
active: {
fill: (d) => d.style?.fill,
stroke: (d) => d.style?.stroke,
lineWidth: 3,
lineWidth: 2,
shadowColor: '#ffffff',
shadowBlur: 10,
opacity: 1
},
inactive: { opacity: 0.3 },
normal: {
fill: (d) => d.style?.fill,
stroke: (d) => d.style?.stroke,
inactive: {
opacity: 0.3
},
normal:{
opacity: 1
}
},
},
edge: {
style: {
stroke: (d) => d.style?.stroke,
lineWidth: (d) => d.style?.lineWidth,
endArrow: (d) => d.style?.endArrow,
labelText: (d) => d.data?.label,
labelFill: (d) => d.style?.labelFill,
labelFontSize: (d) => d.style?.labelFontSize,
labelText: (d) => {
return d.data.label},
// stroke: (d) => {
// const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
// if (targetLabel === '') return 'rgba(239,68,68,0.5)';
// if (targetLabel === ''||targetLabel === '') return 'rgba(145,204,117,0.5)';
// if (targetLabel === '') return 'rgba(250,200,88,0.5)';
// if (targetLabel === '') return 'rgba(51,110,238,0.5)'; //
// return 'rgba(89,209,212,0.5)'; // default
// },
// labelFill: (d) => {
// // target label
// const targetLabel = this._nodeLabelMap.get(d.target); // d.target ID
// // target
//
// if (targetLabel === 'Disease') return '#ff4444';
// if (targetLabel === 'Drug') return '#2f9b70';
// if (targetLabel === 'Symptom') return '#f89775';
// return '#6b91ff'; // default
// }
},
state: {
selected: { stroke: '#1890FF', lineWidth: 2 },
selected: {
stroke: '#1890FF',
lineWidth: 2,
},
highlight: {
halo: true,
haloStroke: '#1890FF',
@ -547,22 +508,36 @@ export default {
lineWidth: 3,
opacity: 1
},
inactive: { opacity: 0.3 },
normal: { opacity: 1 }
inactive: {
opacity: 0.3
},
normal:{
opacity: 1
}
},
data: finalData,
},
data:updatedData,
});
graph.render();
this._graph = graph;
this._graph?.fitView();
this._graph = graph
this._graph?.fitView()
}
},
},
};
</script>
<style scoped>
.medical-qa-container {
padding: 25px;
background-color: #F6F9FF;
@ -749,29 +724,4 @@ export default {
.dot-2 { animation-delay: -0.2s; }
.dot-3 { animation-delay: 0s; }
.dot-4 { animation-delay: 0.2s; }
.knowledge-graph {
position: relative; /* 必须!否则工具栏会飘走 */
flex: 1;
height: 100%;
background-color: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
overflow: hidden; /* 保证工具栏不超出边界 */
}
/* 确保图谱容器铺满 */
.graph-container {
width: 100%;
height: 100%;
}
:deep(.custom-graph-toolbar) {
position: absolute !important; /* 强制绝对定位 */
top: 30px !important;
right: 30px !important;
z-index: 9999 !important; /* 确保层级最高 */
display: inline-flex !important;
}
</style>

30
vue/src/system/GraphStyle.vue

@ -53,7 +53,7 @@
<label>字体颜色</label>
<div class="color-picker-border">
<el-color-picker v-model="nodeFontColor" show-alpha class="square-picker"/>
<!-- <input v-model="nodeFontColor" type="color" class="square-picker"/>-->
<!-- <input v-model="nodeFontColor" type="color" class="square-picker"/>-->
</div>
</div>
@ -82,7 +82,7 @@
<label>填充颜色</label>
<div class="color-picker-border">
<el-color-picker v-model="nodeFill" show-alpha class="square-picker"/>
<!-- <input v-model="nodeFill" type="color" class="square-picker"/>-->
<!-- <input v-model="nodeFill" type="color" class="square-picker"/>-->
</div>
</div>
@ -90,7 +90,7 @@
<label>边框颜色</label>
<div class="color-picker-border">
<el-color-picker v-model="nodeStroke" show-alpha class="square-picker"/>
<!-- <input v-model="nodeStroke" type="color" class="square-picker"/>-->
<!-- <input v-model="nodeStroke" type="color" class="square-picker"/>-->
</div>
</div>
@ -140,7 +140,7 @@
<label>字体颜色</label>
<div class="color-picker-border">
<el-color-picker v-model="edgeFontColor" class="square-picker" show-alpha />
<!-- <input v-model="edgeFontColor" type="color" class="square-picker"/>-->
<!-- <input v-model="edgeFontColor" type="color" class="square-picker"/>-->
</div>
</div>
@ -169,7 +169,7 @@
<label>线条颜色</label>
<div class="color-picker-border">
<el-color-picker v-model="edgeStroke" class="square-picker" show-alpha />
<!-- <input v-model="edgeStroke" type="color" class="square-picker"/>-->
<!-- <input v-model="edgeStroke" type="color" class="square-picker"/>-->
</div>
</div>
</div>
@ -1034,7 +1034,7 @@ export default {
}
};
});
this._graph.setData({nodes, edges});
this._graph.setData({ nodes, edges });
this._graph.render();
},
safeNum(val, defaultVal = 1) {
@ -1050,7 +1050,7 @@ export default {
...conf,
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles
}));
return {...group, configs: uniqueConfigs};
return { ...group, configs: uniqueConfigs };
});
const activeGroup = this.styleGroups.find(g => g.is_active);
@ -1103,27 +1103,19 @@ export default {
const isLabelExist = group.configs.some(c => c.current_label === labelName && c.id !== excludeId);
if (isLabelExist) {
ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置。`, '校验失败', {type: 'error'}).catch(() => {
});
ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置,请先删除旧配置或选择其他方案。`, '校验失败', { type: 'error' }).catch(() => {});
return false;
}
if (group.configs.length >= 5 && !group.configs.some(c => c.id === excludeId)) {
ElMessageBox.alert(`方案【${groupName}】已满(上限5个)。`, '校验失败', {type: 'error'}).catch(() => {
});
ElMessageBox.alert(`方案【${groupName}】的配置已满(上限5个),无法添加。`, '校验失败', { type: 'error' }).catch(() => {});
return false;
}
return true;
},
async moveConfigToGroup(config, targetGroup) {
// 1.
if (config.group_id === targetGroup.id) {
return ElMessage.info("该配置已在该方案中");
}
if (!this.validateGroupConstraint(targetGroup.group_name, config.current_label, config.id)) return;
try {
// 2. Payload target_group_id
const payload = {
@ -1186,8 +1178,7 @@ export default {
const missingTags = REQUIRED_TAGS.filter(tag => !new Set(currentLabels).has(tag));
if (missingTags.length > 0) {
this.isInitialEcho = false;
return ElMessageBox.alert(`该方案配置不完整,缺失:${missingTags.join('、')}`, '提示', {type: 'warning'}).catch(() => {
});
return ElMessageBox.alert(`该方案配置不完整,缺失:${missingTags.join('、')}`, '提示', { type: 'warning' }).catch(() => {});
}
this.usingConfigIds = group.configs.map(c => c.id);
const res = await applyGraphStyleGroup(group.id);
@ -1774,7 +1765,6 @@ export default {
border-color: #e6e6e6 !important;
color: #333 !important;
}
:deep(.el-message-box__btns .el-button--primary) {
background-color: #1559f3 !important;
border-color: #1559f3 !important;

Loading…
Cancel
Save