5 changed files with 1080 additions and 145 deletions
@ -0,0 +1,272 @@ |
|||||
|
<template> |
||||
|
<div style="display: flex; |
||||
|
height: 100vh;"> |
||||
|
<Menu |
||||
|
:initial-active="1" |
||||
|
/> |
||||
|
<div class="qwen-chat"> |
||||
|
<!-- 聊天消息区域 --> |
||||
|
<div ref="messagesContainer" class="chat-messages"> |
||||
|
<div |
||||
|
v-for="(msg, index) in messages" |
||||
|
:key="index" |
||||
|
:class="['message', msg.role]" |
||||
|
> |
||||
|
<div class="bubble">{{ msg.content }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 输入区域(Qwen 风格) --> |
||||
|
<div class="input-area"> |
||||
|
<textarea |
||||
|
v-model="inputText" |
||||
|
placeholder="向千问提问" |
||||
|
class="input-box" |
||||
|
@keyup.enter.exact.prevent="sendMessage" |
||||
|
rows="1" |
||||
|
></textarea> |
||||
|
|
||||
|
<div class="action-buttons"> |
||||
|
<button class="btn" @click="insertPrefix('深度思考:')"> |
||||
|
<span>☰</span> 深度思考 |
||||
|
</button> |
||||
|
<button class="btn" @click="insertPrefix('深度研究:')"> |
||||
|
<span>🔍</span> 深度研究 |
||||
|
</button> |
||||
|
<button class="btn" @click="insertPrefix('```代码\n')"> |
||||
|
<span><></span> 代码 |
||||
|
</button> |
||||
|
<button class="btn" @click="insertPrefix('生成一张图片:')"> |
||||
|
<span>🖼️</span> 图像 |
||||
|
</button> |
||||
|
<button class="btn" @click="insertPrefix('翻译成英文:')"> |
||||
|
<span>🌐</span> 翻译 |
||||
|
</button> |
||||
|
<button class="btn more-btn">...</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="icon-group"> |
||||
|
<span class="icon" title="切换语言">🌍</span> |
||||
|
<span class="icon" title="编辑模式">✏️</span> |
||||
|
<button class="send-btn" @click="sendMessage" title="发送"> |
||||
|
<span>📤</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import Menu from "@/components/Menu.vue"; |
||||
|
|
||||
|
export default { |
||||
|
name: 'QwenChat', |
||||
|
components: {Menu}, |
||||
|
data() { |
||||
|
return { |
||||
|
inputText: '', |
||||
|
messages: [ |
||||
|
{ role: 'assistant', content: '你好!我是 Qwen,有什么可以帮你的吗?' } |
||||
|
] |
||||
|
}; |
||||
|
}, |
||||
|
methods: { |
||||
|
sendMessage() { |
||||
|
const text = this.inputText.trim(); |
||||
|
if (!text) return; |
||||
|
|
||||
|
// 添加用户消息 |
||||
|
this.messages.push({ role: 'user', content: text }); |
||||
|
this.inputText = ''; |
||||
|
|
||||
|
// 模拟 AI 回复(可替换为真实 API) |
||||
|
setTimeout(() => { |
||||
|
this.messages.push({ |
||||
|
role: 'assistant', |
||||
|
content: `你刚才说:“${text}” —— 这是一个模拟回复。` |
||||
|
}); |
||||
|
}, 600); |
||||
|
}, |
||||
|
insertPrefix(prefix) { |
||||
|
this.inputText += prefix; |
||||
|
this.$nextTick(() => { |
||||
|
// 自动聚焦到 textarea 末尾(简单处理) |
||||
|
const el = this.$el.querySelector('.input-box'); |
||||
|
el.focus(); |
||||
|
}); |
||||
|
}, |
||||
|
scrollToBottom() { |
||||
|
this.$nextTick(() => { |
||||
|
const container = this.$refs.messagesContainer; |
||||
|
if (container) { |
||||
|
container.scrollTop = container.scrollHeight; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
messages() { |
||||
|
this.scrollToBottom(); |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.scrollToBottom(); |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.qwen-chat { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100vh; |
||||
|
max-width: 800px; |
||||
|
margin: 0 auto; |
||||
|
border: 1px solid #e0e0e0; |
||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
||||
|
} |
||||
|
|
||||
|
/* === 消息区域 === */ |
||||
|
.chat-messages { |
||||
|
flex: 1; |
||||
|
padding: 16px; |
||||
|
overflow-y: auto; |
||||
|
background-color: #f9f9f9; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.message { |
||||
|
display: flex; |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
|
||||
|
.message.user { |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
|
||||
|
.message.assistant { |
||||
|
justify-content: flex-start; |
||||
|
} |
||||
|
|
||||
|
.bubble { |
||||
|
padding: 10px 14px; |
||||
|
border-radius: 18px; |
||||
|
max-width: 75%; |
||||
|
word-break: break-word; |
||||
|
line-height: 1.5; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.message.user .bubble { |
||||
|
background-color: #007bff; |
||||
|
color: white; |
||||
|
border-bottom-right-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.message.assistant .bubble { |
||||
|
background-color: #e9ecef; |
||||
|
color: #333; |
||||
|
border-bottom-left-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
/* === 输入区域 === */ |
||||
|
.input-area { |
||||
|
padding: 16px; |
||||
|
background: white; |
||||
|
border-top: 1px solid #eee; |
||||
|
} |
||||
|
|
||||
|
.input-box { |
||||
|
width: 100%; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 12px; |
||||
|
font-size: 14px; |
||||
|
line-height: 1.5; |
||||
|
resize: none; |
||||
|
outline: none; |
||||
|
transition: border-color 0.2s ease; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.input-box:focus { |
||||
|
border-color: #007bff; |
||||
|
} |
||||
|
|
||||
|
.action-buttons { |
||||
|
display: flex; |
||||
|
gap: 8px; |
||||
|
margin: 12px 0; |
||||
|
overflow-x: auto; |
||||
|
white-space: nowrap; |
||||
|
padding: 0 4px; |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
padding: 6px 12px; |
||||
|
background: #f5f5f5; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 12px; |
||||
|
font-size: 12px; |
||||
|
color: #333; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
} |
||||
|
|
||||
|
.btn:hover { |
||||
|
background: #e9e9e9; |
||||
|
} |
||||
|
|
||||
|
.btn span { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.more-btn { |
||||
|
background: transparent; |
||||
|
border: 1px dashed #ccc; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.icon-group { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.icon, |
||||
|
.send-btn span { |
||||
|
font-size: 16px; |
||||
|
cursor: pointer; |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
.icon:hover, |
||||
|
.send-btn:hover span { |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.send-btn { |
||||
|
background: #9b68ff; |
||||
|
border: none; |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
color: white; |
||||
|
cursor: pointer; |
||||
|
transition: background-color 0.2s; |
||||
|
} |
||||
|
|
||||
|
.send-btn:hover { |
||||
|
background: #8a53e0; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,788 @@ |
|||||
|
<template> |
||||
|
<div class="knowledge-graph-container"> |
||||
|
<Menu |
||||
|
:initial-active="5" |
||||
|
/> |
||||
|
<div class="control-panel"> |
||||
|
<!-- 节点控制 --> |
||||
|
<div class="section"> |
||||
|
<h5>节点控制</h5> |
||||
|
<label style="text-align: left;margin: 10px 0;display: flex;align-items: center;font-size: 12px;"> |
||||
|
<input v-model="nodeShowLabel" type="checkbox" /> |
||||
|
显示节点标签 |
||||
|
</label> |
||||
|
<div class="form-group"> |
||||
|
<label>字体名称:</label> |
||||
|
<select v-model="nodeFontFamily"> |
||||
|
<!-- 中文常用字体 --> |
||||
|
<option value="Microsoft YaHei, sans-serif">微软雅黑</option> |
||||
|
<option value="SimSun, serif">宋体(SimSun)</option> |
||||
|
<option value="SimHei, sans-serif">黑体(SimHei)</option> |
||||
|
<option value="KaiTi, serif">楷体(KaiTi)</option> |
||||
|
<option value="FangSong, serif">仿宋(FangSong)</option> |
||||
|
<option value="PingFang SC, Helvetica Neue, sans-serif">苹方(PingFang SC)</option> |
||||
|
|
||||
|
<!-- 英文/通用字体 --> |
||||
|
<option value="Arial, sans-serif">Arial</option> |
||||
|
<option value="Helvetica, sans-serif">Helvetica</option> |
||||
|
<option value="Times New Roman, serif">Times New Roman</option> |
||||
|
<option value="Georgia, serif">Georgia</option> |
||||
|
<option value="Courier New, monospace">Courier New(等宽)</option> |
||||
|
<option value="Verdana, sans-serif">Verdana</option> |
||||
|
<option value="Tahoma, sans-serif">Tahoma</option> |
||||
|
<option value="Impact, sans-serif">Impact</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="slider-group"> |
||||
|
<span>字体大小: {{ nodeFontSize }}px</span> |
||||
|
<input |
||||
|
v-model.number="nodeFontSize" |
||||
|
type="range" |
||||
|
min="10" |
||||
|
max="24" |
||||
|
step="1" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="color-picker"> |
||||
|
<label>字体颜色:</label> |
||||
|
<input v-model="nodeFontColor" type="color" /> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label>图形:</label> |
||||
|
<select v-model="nodeShape"> |
||||
|
<option value="circle">圆形</option> |
||||
|
<option value="diamond">菱形</option> |
||||
|
<option value="triangle">三角形</option> |
||||
|
<option value="rect">矩形</option> |
||||
|
<option value="star">星形</option> |
||||
|
<option value="hexagon">六边形</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label>尺寸:</label> |
||||
|
<input v-model.number="nodeSize" type="number" min="30" max="100" /> |
||||
|
</div> |
||||
|
<div class="color-picker"> |
||||
|
<label>填充颜色:</label> |
||||
|
<input v-model="nodeFill" type="color" /> |
||||
|
</div> |
||||
|
<div class="color-picker"> |
||||
|
<label>边框颜色:</label> |
||||
|
<input v-model="nodeStroke" type="color" /> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label>边框尺寸:</label> |
||||
|
<input v-model.number="nodeLineWidth" type="number" min="1" max="5" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 连边控制 --> |
||||
|
<div class="section"> |
||||
|
<h5>连边控制</h5> |
||||
|
<label style="text-align: left;margin: 10px 0;display: flex;align-items: center;font-size: 12px;"> |
||||
|
<input v-model="edgeShowLabel" type="checkbox" /> |
||||
|
显示连边标签 |
||||
|
<input v-model="edgeEndArrow" type="checkbox" style="margin-left: 10px"/> |
||||
|
显示端点箭头 |
||||
|
</label> |
||||
|
<div class="form-group"> |
||||
|
<label>字体名称:</label> |
||||
|
<select v-model="edgeFontFamily"> |
||||
|
<option value="Microsoft YaHei, sans-serif">微软雅黑</option> |
||||
|
<option value="SimSun, serif">宋体</option> |
||||
|
<option value="SimHei, sans-serif">黑体</option> |
||||
|
<option value="Arial, sans-serif">Arial</option> |
||||
|
<option value="sans-serif">无衬线(默认)</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="slider-group"> |
||||
|
<span>字体大小: {{ edgeFontSize }}px</span> |
||||
|
<input |
||||
|
v-model.number="edgeFontSize" |
||||
|
type="range" |
||||
|
min="8" |
||||
|
max="16" |
||||
|
step="1" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="color-picker"> |
||||
|
<label>字体颜色:</label> |
||||
|
<input v-model="edgeFontColor" type="color" /> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label>连边类型:</label> |
||||
|
<select v-model="edgeType"> |
||||
|
<option value="line">直线</option> |
||||
|
<option value="polyline">折线边</option> |
||||
|
<option value="cubic">三次贝塞尔曲线边</option> |
||||
|
<option value="cubic-horizontal">水平三次贝塞尔曲线边</option> |
||||
|
<option value="cubic-vertical">垂直三次贝塞尔曲线边</option> |
||||
|
<option value="quadratic">二次贝塞尔曲线边</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label>线粗细:</label> |
||||
|
<input v-model.number="edgeLineWidth" type="number" min="1" max="5" /> |
||||
|
</div> |
||||
|
<div class="color-picker"> |
||||
|
<label>线条颜色:</label> |
||||
|
<input v-model="edgeStroke" type="color" /> |
||||
|
</div> |
||||
|
<!-- <label>--> |
||||
|
<!-- <input v-model="edgeEndArrow" type="checkbox" />--> |
||||
|
<!-- 显示端点箭头--> |
||||
|
<!-- </label>--> |
||||
|
</div> |
||||
|
<button @click="resetView">重置视图</button> |
||||
|
<button @click="resetStyle">重置样式</button> |
||||
|
<!-- 操作按钮 --> |
||||
|
</div> |
||||
|
<!-- 图谱容器 --> |
||||
|
<div ref="graphContainer" class="graph-container" id="container"></div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { getTestGraphData } from "@/api/graph" |
||||
|
import { Graph } from '@antv/g6'; |
||||
|
import Menu from "@/components/Menu.vue"; |
||||
|
|
||||
|
export default { |
||||
|
name: 'GraphDemo', |
||||
|
components: {Menu}, |
||||
|
data() { |
||||
|
return { |
||||
|
G6: null, // 添加这个 |
||||
|
// 节点样式 |
||||
|
nodeShowLabel: true, |
||||
|
nodeFontSize: 12, |
||||
|
nodeFontColor: '#fff', |
||||
|
nodeShape: 'circle', |
||||
|
nodeSize: 60, |
||||
|
nodeFill: '#9FD5FF', |
||||
|
nodeStroke: '#5B8FF9', |
||||
|
nodeLineWidth: 2, |
||||
|
nodeFontFamily: 'Microsoft YaHei, sans-serif', |
||||
|
|
||||
|
// 边样式 |
||||
|
edgeShowLabel: true, |
||||
|
edgeFontSize: 10, |
||||
|
edgeFontColor: '#666666', |
||||
|
edgeType: 'line', |
||||
|
edgeLineWidth: 2, |
||||
|
edgeStroke: '#F00', |
||||
|
edgeEndArrow: true, |
||||
|
edgeFontFamily: 'Microsoft YaHei, sans-serif', |
||||
|
|
||||
|
defaultData: { |
||||
|
"nodes": [ |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"data": { |
||||
|
"name": "霍乱", |
||||
|
"diagnosisCode": "A00.900", |
||||
|
"nodeId": 6978, |
||||
|
"label": "Disease" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:9461", |
||||
|
"data": { |
||||
|
"name": "腹泻", |
||||
|
"nodeId": 9464, |
||||
|
"label": "Symptom" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:5992", |
||||
|
"data": { |
||||
|
"name": "脱水", |
||||
|
"nodeId": 5995, |
||||
|
"label": "Disease" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:1277", |
||||
|
"data": { |
||||
|
"name": "呕吐", |
||||
|
"nodeId": 1280, |
||||
|
"label": "Disease" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:26775", |
||||
|
"data": { |
||||
|
"name": "由霍乱弧菌感染所致", |
||||
|
"nodeId": 26778, |
||||
|
"label": "Cause" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:48981", |
||||
|
"data": { |
||||
|
"name": "复方磺胺甲噁唑", |
||||
|
"nodeId": 48984, |
||||
|
"label": "Drug" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:17392", |
||||
|
"data": { |
||||
|
"name": "消化系统", |
||||
|
"nodeId": 17395, |
||||
|
"label": "DiseaseSite" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:18534", |
||||
|
"data": { |
||||
|
"name": "传染科", |
||||
|
"nodeId": 18537, |
||||
|
"label": "Department" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:413", |
||||
|
"data": { |
||||
|
"name": "代谢性 酸中毒", |
||||
|
"nodeId": 416, |
||||
|
"label": "Disease" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"id": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:2990", |
||||
|
"data": { |
||||
|
"name": "急性肾衰竭", |
||||
|
"nodeId": 2993, |
||||
|
"label": "Disease" |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"edges": [ |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:9461", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "symptomAndSign", |
||||
|
"properties": { "label": "症状与体征" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:5992", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "complication", |
||||
|
"properties": { "label": "并发症" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:1277", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "complication", |
||||
|
"properties": { "label": "并发症" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:26775", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "cause", |
||||
|
"properties": { "label": "病因" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:48981", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "treatmentPrograms", |
||||
|
"properties": { "label": "治疗方案" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:17392", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "diseaseSite", |
||||
|
"properties": { "label": "病变部位" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:18534", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "department", |
||||
|
"properties": { "label": "科室" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:413", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "complication", |
||||
|
"properties": { "label": "并发症" } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"source": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:6975", |
||||
|
"target": "4:bc93ccaa-d618-48f0-b787-2d9496417bcd:2990", |
||||
|
"type": "line", |
||||
|
"data": { |
||||
|
"relationship": { |
||||
|
"type": "complication", |
||||
|
"properties": { "label": "并发症" } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
children: 'children', |
||||
|
label: 'title' // 虽然用插槽,但 el-tree 内部仍会读取,可留空或任意 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
async mounted() { |
||||
|
await this.$nextTick(); |
||||
|
try { |
||||
|
const updatedNodes = this.defaultData.nodes.map(node => ({ |
||||
|
...node, |
||||
|
type: this.nodeShape, |
||||
|
style:{ |
||||
|
size: this.nodeSize, |
||||
|
fill: this.nodeFill, |
||||
|
stroke: this.nodeStroke, |
||||
|
lineWidth: this.nodeLineWidth, |
||||
|
label: this.nodeShowLabel, |
||||
|
labelFontSize: this.nodeFontSize, |
||||
|
labelFontFamily: this.nodeFontFamily, |
||||
|
labelFill: this.nodeFontColor, |
||||
|
} |
||||
|
})) |
||||
|
const updatedEdges = this.defaultData.edges.map(edge => ({ |
||||
|
...edge, |
||||
|
type: this.edgeType, |
||||
|
id: edge.data.relationship.id, |
||||
|
style: { |
||||
|
endArrow: this.edgeEndArrow, |
||||
|
stroke: this.edgeStroke, |
||||
|
lineWidth: this.edgeLineWidth, |
||||
|
label: this.edgeShowLabel, |
||||
|
labelFontSize: this.edgeFontSize, |
||||
|
labelFontFamily: this.edgeFontFamily, |
||||
|
labelFill: this.edgeFontColor, |
||||
|
}, |
||||
|
})) |
||||
|
const updatedData = { |
||||
|
nodes: updatedNodes, |
||||
|
edges: updatedEdges |
||||
|
} |
||||
|
this.defaultData = updatedData |
||||
|
setTimeout(() => { |
||||
|
this.initGraph(); |
||||
|
window.addEventListener('resize', this.handleResize); |
||||
|
}, 1000); |
||||
|
} catch (error) { |
||||
|
console.error('加载图谱数据失败:', error); |
||||
|
} |
||||
|
}, |
||||
|
beforeUnmount() { |
||||
|
this._graph.destroy() |
||||
|
this._graph = null; |
||||
|
window.removeEventListener('resize', this.handleResize); |
||||
|
}, |
||||
|
watch: { |
||||
|
// 节点相关 |
||||
|
nodeShowLabel: 'updateAllNodes', |
||||
|
nodeFontSize: 'updateAllNodes', |
||||
|
nodeFontColor: 'updateAllNodes', |
||||
|
nodeShape: 'updateAllNodes', |
||||
|
nodeSize: 'updateAllNodes', |
||||
|
nodeFill: 'updateAllNodes', |
||||
|
nodeStroke: 'updateAllNodes', |
||||
|
nodeLineWidth: 'updateAllNodes', |
||||
|
nodeFontFamily: 'updateAllNodes', |
||||
|
|
||||
|
// 边相关 |
||||
|
edgeShowLabel: 'updateAllEdges', |
||||
|
edgeFontSize: 'updateAllEdges', |
||||
|
edgeFontColor: 'updateAllEdges', |
||||
|
edgeType: 'updateAllEdges', |
||||
|
edgeLineWidth: 'updateAllEdges', |
||||
|
edgeStroke: 'updateAllEdges', |
||||
|
edgeEndArrow: 'updateAllEdges', |
||||
|
edgeFontFamily: 'updateAllEdges', |
||||
|
}, |
||||
|
methods: { |
||||
|
buildNodeLabelMap(nodes) { |
||||
|
this._nodeLabelMap = new Map(); |
||||
|
nodes.forEach(node => { |
||||
|
this._nodeLabelMap.set(node.id, node.data?.label || 'default'); |
||||
|
}); |
||||
|
}, |
||||
|
initGraph() { |
||||
|
|
||||
|
if (this._graph!=null){ |
||||
|
this._graph.destroy() |
||||
|
this._graph = null; |
||||
|
} |
||||
|
if (!this._nodeLabelMap) { |
||||
|
this.buildNodeLabelMap(this.defaultData.nodes); |
||||
|
} |
||||
|
const container = this.$refs.graphContainer; |
||||
|
const width = container.clientWidth || 800; |
||||
|
const height = container.clientHeight || 600; |
||||
|
|
||||
|
const graph = new Graph({ |
||||
|
container, |
||||
|
width, |
||||
|
height, |
||||
|
layout: { |
||||
|
type: 'radial', |
||||
|
nodeSize: 32, |
||||
|
unitRadius: 100, |
||||
|
linkDistance: 200, |
||||
|
}, |
||||
|
behaviors: [ 'zoom-canvas', 'drag-element','click-select','focus-element', |
||||
|
{ |
||||
|
type: 'hover-activate', |
||||
|
degree: 1, // 👈🏻 Activate relations. |
||||
|
}, |
||||
|
{ |
||||
|
type: 'drag-canvas', |
||||
|
enable: (event) => event.shiftKey === false, |
||||
|
}, |
||||
|
{ |
||||
|
type: 'brush-select', |
||||
|
}, |
||||
|
], |
||||
|
|
||||
|
node: { |
||||
|
style: { |
||||
|
labelText: (d) => d.data.name, |
||||
|
labelPlacement: 'center', |
||||
|
fill: (d) => { |
||||
|
const label = d.data?.label; |
||||
|
if (label === 'Disease') return '#EF4444'; // 红 |
||||
|
if (label === 'Drug') return '#91cc75'; // 绿 |
||||
|
if (label === 'Symptom') return '#fac858'; // 橙 |
||||
|
if (label === 'Check') return '#59d1d4'; // 橙 |
||||
|
return '#336eee'; // 默认灰蓝 |
||||
|
}, |
||||
|
stroke: (d) => { |
||||
|
const label = d.data?.label; |
||||
|
if (label === 'Disease') return '#B91C1C'; |
||||
|
if (label === 'Drug') return '#047857'; |
||||
|
if (label === 'Check') return '#40999b'; // 橙 |
||||
|
if (label === 'Symptom') return '#B45309'; |
||||
|
return '#1D4ED8'; |
||||
|
}, |
||||
|
labelWordWrap: true, |
||||
|
labelMaxWidth: '150%', |
||||
|
labelMaxLines: 3, |
||||
|
labelTextOverflow: 'ellipsis', |
||||
|
labelTextAlign: 'center', |
||||
|
}, |
||||
|
state: { |
||||
|
|
||||
|
highlight: { |
||||
|
stroke: '#FF6A00', |
||||
|
lineWidth: 3, |
||||
|
}, |
||||
|
disabled: { |
||||
|
fill: '#ECECEC', |
||||
|
stroke: '#BFBFBF', |
||||
|
opacity: 0.5, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
edge: { |
||||
|
style: { |
||||
|
labelText: (d) => d.data.relationship.properties.label, |
||||
|
stroke: (d) => { |
||||
|
// 获取 target 节点的 label |
||||
|
const targetLabel = this._nodeLabelMap.get(d.target); // d.target 是目标节点 ID |
||||
|
// 根据 target 节点类型返回对应浅色 |
||||
|
if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)'; |
||||
|
if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)'; |
||||
|
if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)'; |
||||
|
if (targetLabel === 'Check') return 'rgba(89,209,212,0.5)'; // 橙 |
||||
|
return 'rgba(51,110,238,0.5)'; // default |
||||
|
}, |
||||
|
}, |
||||
|
state: { |
||||
|
selected: { |
||||
|
stroke: '#1890FF', |
||||
|
lineWidth: 2, |
||||
|
}, |
||||
|
highlight: { |
||||
|
stroke: '#FF6A00', |
||||
|
lineWidth: 3, |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
}, |
||||
|
data:this.defaultData |
||||
|
}); |
||||
|
|
||||
|
graph.render(); |
||||
|
this._graph = graph |
||||
|
this._graph?.fitView() |
||||
|
}, |
||||
|
|
||||
|
updateGraph(data) { |
||||
|
if (!this._graph) return |
||||
|
this._graph.setData(data) |
||||
|
this._graph.render() |
||||
|
}, |
||||
|
|
||||
|
updateAllNodes() { |
||||
|
if (!this._graph) return |
||||
|
const updatedNodes = this.defaultData.nodes.map(node => ({ |
||||
|
...node, |
||||
|
type: this.nodeShape, |
||||
|
style:{ |
||||
|
size: this.nodeSize, |
||||
|
fill: this.nodeFill, |
||||
|
stroke: this.nodeStroke, |
||||
|
lineWidth: this.nodeLineWidth, |
||||
|
label: this.nodeShowLabel, |
||||
|
labelFontSize: this.nodeFontSize, |
||||
|
labelFontFamily: this.nodeFontFamily, |
||||
|
labelFontColor: this.nodeFontColor, |
||||
|
} |
||||
|
})) |
||||
|
const updatedData = { |
||||
|
nodes: updatedNodes, |
||||
|
edges: this.defaultData.edges |
||||
|
} |
||||
|
this.defaultData = updatedData |
||||
|
this.updateGraph(updatedData) |
||||
|
}, |
||||
|
|
||||
|
updateAllEdges() { |
||||
|
if (!this._graph) return |
||||
|
const updatedEdges = this.defaultData.edges.map(edge => ({ |
||||
|
...edge, |
||||
|
type:this.edgeType, |
||||
|
style: { |
||||
|
endArrow: this.edgeEndArrow, |
||||
|
stroke: this.edgeStroke, |
||||
|
lineWidth: this.edgeLineWidth, |
||||
|
label: this.edgeShowLabel, |
||||
|
labelFontSize: this.edgeFontSize, |
||||
|
labelFontFamily: this.edgeFontFamily, |
||||
|
labelFill: this.edgeFontColor, |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
})) |
||||
|
const updatedData = { |
||||
|
nodes: this.defaultData.nodes, |
||||
|
edges: updatedEdges |
||||
|
} |
||||
|
this.defaultData = updatedData |
||||
|
this.updateGraph(updatedData) |
||||
|
}, |
||||
|
|
||||
|
clearAllStates() { |
||||
|
if (!this._graph) return |
||||
|
|
||||
|
// 清除所有节点的状态 |
||||
|
this._graph.getNodes().forEach(node => { |
||||
|
this._graph.clearItemStates(node) |
||||
|
}) |
||||
|
|
||||
|
// 清除所有边的状态 |
||||
|
this._graph.getEdges().forEach(edge => { |
||||
|
this._graph.clearItemStates(edge) |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
handleResize() { |
||||
|
const container = this.$refs.graphContainer |
||||
|
if (this._graph && container) { |
||||
|
this._graph.resize(container.offsetWidth, container.offsetHeight) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
resetView() { |
||||
|
this._graph?.fitView() |
||||
|
}, |
||||
|
|
||||
|
resetStyle() { |
||||
|
Object.assign(this, { |
||||
|
nodeShowLabel: true, |
||||
|
nodeFontSize: 12, |
||||
|
nodeFontColor: '#000000', |
||||
|
nodeShape: 'circle', |
||||
|
nodeSize: 60, |
||||
|
nodeFill: '#9FD5FF', |
||||
|
nodeStroke: '#5B8FF9', |
||||
|
nodeLineWidth: 2, |
||||
|
nodeFontFamily: 'Microsoft YaHei, sans-serif', |
||||
|
|
||||
|
edgeShowLabel: true, |
||||
|
edgeFontSize: 10, |
||||
|
edgeFontColor: '#666666', |
||||
|
edgeType: 'line', |
||||
|
edgeLineWidth: 2, |
||||
|
edgeStroke: '#F00', |
||||
|
edgeEndArrow: true, |
||||
|
edgeFontFamily: 'Microsoft YaHei, sans-serif' |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
|
||||
|
handleNodeClick(data) { |
||||
|
console.log('点击节点:', data) |
||||
|
// 可用于显示详情、复制 code 等 |
||||
|
this.$message.info(`已选中: ${data.title}`) |
||||
|
}, |
||||
|
|
||||
|
getLevelLabel(level) { |
||||
|
const map = { |
||||
|
chapter: '章', |
||||
|
section: '节', |
||||
|
category: '类目', |
||||
|
subcategory: '亚目', |
||||
|
diagnosis: '条目' |
||||
|
} |
||||
|
return map[level] || level |
||||
|
}, |
||||
|
|
||||
|
getTagType(level) { |
||||
|
const map = { |
||||
|
chapter: 'primary', |
||||
|
section: 'success', |
||||
|
category: 'warning', |
||||
|
subcategory: 'info', |
||||
|
diagnosis: '' |
||||
|
} |
||||
|
return map[level] || '' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.knowledge-graph-container { |
||||
|
display: flex; |
||||
|
height: 100vh; |
||||
|
} |
||||
|
|
||||
|
.control-panel { |
||||
|
width: 280px; |
||||
|
background: #f5f7fa; |
||||
|
padding: 16px; |
||||
|
border-right: 1px solid #ddd; |
||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); |
||||
|
font-family: 'Microsoft YaHei'; |
||||
|
} |
||||
|
|
||||
|
.section { |
||||
|
margin-bottom: 10px; |
||||
|
padding: 10px; |
||||
|
background: white; |
||||
|
border-radius: 8px; |
||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.section h5 { |
||||
|
margin-top: 0; |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.slider-group { |
||||
|
margin: 10px 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.slider-group span { |
||||
|
margin-right: 10px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
margin: 10px 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.form-group label { |
||||
|
margin-right: 8px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.form-group select, |
||||
|
.form-group input[type="number"] { |
||||
|
padding: 4px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.color-picker { |
||||
|
margin: 10px 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.color-picker label { |
||||
|
margin-right: 8px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.color-picker input[type="color"] { |
||||
|
width: 40px; |
||||
|
height: 20px; |
||||
|
border: none; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
button { |
||||
|
width: 45%; |
||||
|
padding: 10px; |
||||
|
background: #007bff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 6px; |
||||
|
font-size: 14px; |
||||
|
cursor: pointer; |
||||
|
margin-right: 2%; |
||||
|
} |
||||
|
|
||||
|
button:hover { |
||||
|
background: #0056b3; |
||||
|
} |
||||
|
|
||||
|
.graph-container { |
||||
|
flex: 1; |
||||
|
border: 1px solid #eee; |
||||
|
background: #fff; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue