Browse Source

all

yangrongze
hanyuqing 4 months ago
parent
commit
fbb21e4433
  1. 80
      controller/GraphController.py
  2. 7
      util/neo4j_utils.py
  3. 2
      vue/src/App.vue
  4. 6
      vue/src/api/builder.js
  5. BIN
      vue/src/assets/logo1.png
  6. 87
      vue/src/components/Menu.vue
  7. 178
      vue/src/system/GraphBuilder.vue
  8. 451
      vue/src/system/GraphDemo.vue
  9. 85
      vue/src/system/Login.vue
  10. 3
      vue/src/system/Profile.vue
  11. 2
      vue/src/utils/request.js

80
controller/GraphController.py

@ -209,31 +209,75 @@ def health():
return {"status": "ok", "drug_cached": redis_get(DRUG_TREE_KEY) is not None} return {"status": "ok", "drug_cached": redis_get(DRUG_TREE_KEY) is not None}
# @app.post("/api/analyze")
# async def analyze(request):
# # 假设前端传入 JSON: {"text": "病例文本..."}
# body = request.json()
# input_text = body.get("text", "").strip()
#
# if not input_text:
# return jsonify({"error": "缺少 text 字段"}), 400
# client = httpx.AsyncClient(base_url="http://192.168.50.113:8088")
# # 调用实体关系抽取服务
# response = await client.post(
# "/extract_entities_and_relations",
# json={"text": input_text}
# )
# print(response)
# if response.status_code == 200:
# result = response.json()
# return jsonify(result)
# else:
# return jsonify({
# "error": "实体抽取服务返回错误",
# "status": response.status_code,
# "detail": response.text
# }), response.status_code
# 全局 client(可复用)
client = httpx.AsyncClient(base_url="http://192.168.50.113:8088")
@app.post("/api/analyze") @app.post("/api/analyze")
async def analyze(request): async def analyze(request):
body = request.json()
input_text = body.get("text", "").strip()
if not input_text:
return jsonify({"error": "缺少 text 字段"}), 400
try: try:
# 假设前端传入 JSON: {"text": "病例文本..."} # 直接转发到大模型服务(假设它返回 { "task_id": "xxx" })
body = request.json() resp = await client.post(
input_text = body.get("text", "").strip()
if not input_text:
return jsonify({"error": "缺少 text 字段"}), 400
client = httpx.AsyncClient(base_url="http://192.168.50.113:8088")
# 调用实体关系抽取服务
response = await client.post(
"/extract_entities_and_relations", "/extract_entities_and_relations",
json={"text": input_text} json={"text": input_text},
timeout=1800.0 # 30分钟
) )
print(response) print(resp)
if response.status_code == 200:
result = response.json() if resp.status_code == 202 or resp.status_code == 200:
return jsonify(result) return Response(
status_code=200,
description=jsonify(resp.json()),
headers={"Content-Type": "text/plain; charset=utf-8"}
)
else: else:
return jsonify({ return jsonify({
"error": "实体抽取服务返回错误", "error": "提交失败",
"status": response.status_code, "detail": resp.text
"detail": response.text }), resp.status_code
}), response.status_code except Exception as e:
return jsonify({"error": str(e)}), 500
@app.get("/api/analyze/status")
async def get_status():
try:
# 直接转发状态查询到大模型服务
resp = await client.get(
"/status",
timeout=5.0
)
return jsonify(resp.json()), resp.status_code
except httpx.ReadTimeout:
return jsonify({"error": "状态查询超时"}), 500
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500

7
util/neo4j_utils.py

@ -320,10 +320,8 @@ class Neo4jUtil:
raise RuntimeError( raise RuntimeError(
f"查询邻居节点时发生数据库错误: {str(e)}" f"查询邻居节点时发生数据库错误: {str(e)}"
) from e # 使用 'from e' 保留原始异常链 ) from e # 使用 'from e' 保留原始异常链
print(raw_results)
neighbors = [] neighbors = []
print("Ssssssssss")
print(len(raw_results))
for row in raw_results: for row in raw_results:
source = dict(row["sourceProps"]) source = dict(row["sourceProps"])
source.update({"id": row["sourceId"], "label": row["sourceLabel"]}) source.update({"id": row["sourceId"], "label": row["sourceLabel"]})
@ -339,8 +337,7 @@ class Neo4jUtil:
"target": target, "target": target,
"relationship": relationship "relationship": relationship
}) })
print(relationship)
print(neighbors)
return neighbors return neighbors
# def find_neighbors_with_relationships( # def find_neighbors_with_relationships(
# self, # self,

2
vue/src/App.vue

@ -10,10 +10,10 @@ export default {
<style> <style>
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;
color: #2c3e50; color: #2c3e50;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
} }
</style> </style>

6
vue/src/api/builder.js

@ -13,3 +13,9 @@ export function analyze(data) {
data data
}); });
} }
export function getAIStatus() {
return request({
url: '/api/analyze/status',
method: 'get',
});
}

BIN
vue/src/assets/logo1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

87
vue/src/components/Menu.vue

@ -1,10 +1,13 @@
<template> <template>
<div class="admin-layout"> <div class="admin-layout" :class="{
<aside class="sidebar" :class="{ 'is-collapsed': isCollapsed }"> 'is-collapsed': isCollapsed,
'is-expanded': !isCollapsed
}">
<aside class="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<div class="header-content"> <div class="header-content">
<div class="header-icon-wrap"> <div class="header-icon-wrap">
<img src="@/assets/logo.png" class="logo-img"/> <img src="../assets/logo1.png" class="logo-img"/>
</div> </div>
<div v-if="!isCollapsed" class="header-text-wrap"> <div v-if="!isCollapsed" class="header-text-wrap">
<div class="title-line">面向疾病预测的知识图谱</div> <div class="title-line">面向疾病预测的知识图谱</div>
@ -37,11 +40,14 @@
</div> </div>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer"
:style="{
'border-top': isCollapsed ? 'none' : '2px solid rgba(255, 255, 255, 0.1)'
}">
<div v-if="!isCollapsed" class="user-block"> <div v-if="!isCollapsed" class="user-block">
<div class="avatar">用户</div> <div class="avatar" @click="handleProfile">用户</div>
<div class="info"> <div class="info">
<div class="name">用户名字</div> <div class="name" @click="handleProfile">用户名字</div>
<div class="id">8866990099</div> <div class="id">8866990099</div>
</div> </div>
<div class="exit-wrap"> <div class="exit-wrap">
@ -92,30 +98,39 @@ const menuItems = [
{name: '知识图谱构建', path: '/kg-builder', icon: i2}, {name: '知识图谱构建', path: '/kg-builder', icon: i2},
{name: '知识图谱问答', path: '/kg-qa', icon: i3}, {name: '知识图谱问答', path: '/kg-qa', icon: i3},
{name: '知识图谱数据', path: '/kg-data', icon: i4}, {name: '知识图谱数据', path: '/kg-data', icon: i4},
{name: '知识图谱数据', path: '/kg-data', icon: i4} {name: '图谱样式工具', path: '/kg-style', icon: i4}
]; ];
const handleMenuClick = (i) => { const handleMenuClick = (i) => {
activeIndex.value = i; activeIndex.value = i;
router.push(menuItems[i].path); router.push(menuItems[i].path);
}; };
const handleLogout = () => console.log('logout'); const handleProfile = () => {
// 使Vue Router
router.push('/profile');
};
const handleLogout = () => {
// 使Vue Router
router.push('/login');
};
</script> </script>
<style scoped> <style scoped>
.admin-layout { .admin-layout {
display: flex; display: flex;
width: 200px;
height: 100vh; height: 100vh;
background: #fff; background: #fff;
overflow: hidden; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
z-index: 100;
box-shadow: 2px -1px 10px 8px rgb(97 99 100 / 22%);
width: 12%;
} }
/* --- 侧边栏 --- */ /* --- 侧边栏 --- */
.sidebar { .sidebar {
width: 200px; width: 100%;
height: 100%; height: 100%;
background: #0a2463; background: #0a2463;
flex-shrink: 0; flex-shrink: 0;
@ -126,13 +141,15 @@ const handleLogout = () => console.log('logout');
z-index: 100; z-index: 100;
} }
.sidebar.is-collapsed { .admin-layout.is-collapsed {
width: 64px; width: 4%;
}
.admin-layout.is-expanded {
width: 12%;
} }
.sidebar-header { .sidebar-header {
padding: 30px 0 20px 0; padding: 25px 0px 18px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08); border-bottom: 2px solid rgba(255, 255, 255, 0.1);
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
@ -140,14 +157,13 @@ const handleLogout = () => console.log('logout');
.header-content { .header-content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 7px;
width: fit-content; width: fit-content;
} }
.header-icon-wrap { .header-icon-wrap {
width: 32px; width: 32px;
height: 32px; height: 32px;
background: #165dff;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -157,8 +173,8 @@ const handleLogout = () => console.log('logout');
} }
.logo-img { .logo-img {
width: 22px; width: 29px;
height: 22px; height: 30px;
object-fit: contain; object-fit: contain;
} }
@ -170,10 +186,11 @@ const handleLogout = () => console.log('logout');
.title-line { .title-line {
color: #fff; color: #fff;
font-size: 12px; font-size: 11px;
font-weight: 600;
line-height: 1.25; line-height: 1.25;
white-space: nowrap; white-space: nowrap;
font-weight: 600;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
} }
/* --- 菜单导航 --- */ /* --- 菜单导航 --- */
@ -209,7 +226,7 @@ const handleLogout = () => console.log('logout');
} }
.menu-item:hover .highlight-box { .menu-item:hover .highlight-box {
background: rgba(255, 255, 255, 0.08); background: #133189;
} }
.menu-content-fixed { .menu-content-fixed {
@ -247,9 +264,10 @@ const handleLogout = () => console.log('logout');
.menu-text { .menu-text {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
font-size: 14px;
margin-left: 10px; margin-left: 10px;
font-weight: 400; font-weight: 500;
font-size: 12px;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
} }
.active-tag { .active-tag {
@ -273,7 +291,7 @@ const handleLogout = () => console.log('logout');
/* --- 用户区域 (重点修改) --- */ /* --- 用户区域 (重点修改) --- */
.sidebar-footer { .sidebar-footer {
padding: 12px 0 20px 0; padding: 12px 0 20px 0;
border-top: 1px solid rgba(255, 255, 255, 0.1); border-top: 2px solid rgba(255, 255, 255, 0.1);
} }
.user-block { .user-block {
@ -283,8 +301,8 @@ const handleLogout = () => console.log('logout');
} }
.avatar { .avatar {
width: 42px; width: 36px;
height: 42px; height: 36px;
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
border-radius: 50%; border-radius: 50%;
font-size: 12px; font-size: 12px;
@ -293,26 +311,30 @@ const handleLogout = () => console.log('logout');
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
cursor: pointer;
} }
.info { .info {
margin-left: 12px; margin-left: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: flex-start;
} }
.info .name { .info .name {
font-size: 12px; font-size: 11px;
color: #fff; color: #fff;
line-height: 1; line-height: 1;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
cursor: pointer;
} }
.info .id { .info .id {
font-size: 9px; font-size: 9px;
color: rgba(255, 255, 255, 0.35); color: rgba(255, 255, 255, 0.5);
line-height: 1; line-height: 1;
margin-top: 8px; /* 通过增加上边距让id向下移动 */ margin-top: 6px; /* 通过增加上边距让id向下移动 */
} }
.exit-wrap { .exit-wrap {
@ -322,7 +344,7 @@ const handleLogout = () => console.log('logout');
} }
.exit-icon { .exit-icon {
width: 16px; width: 11px;
cursor: pointer; cursor: pointer;
opacity: 0.8; opacity: 0.8;
} }
@ -330,7 +352,6 @@ const handleLogout = () => console.log('logout');
.expand-handle-circle { .expand-handle-circle {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding-bottom: 20px;
cursor: pointer; cursor: pointer;
} }

178
vue/src/system/GraphBuilder.vue

@ -12,7 +12,46 @@
:key="index" :key="index"
:class="['message', msg.role]" :class="['message', msg.role]"
> >
<div class="bubble">{{ msg.content }}</div> <div v-if="msg.role === 'user'" class="bubble">
{{ msg.content }}
</div>
<div v-else-if="msg.role === 'assistant'">
<div v-if="msg.isKG" class="kg-card">
<h4>🔬 病历结构化分析结果</h4>
<div v-if="msg.content.entities?.length" class="kg-section">
<h5>识别出的实体</h5>
<div class="entity-list">
<span
v-for="(ent, i) in msg.content.entities"
:key="i"
class="entity-tag"
:class="'tag-' + ent.t"
>
{{ ent.n }}<small>({{ ent.t }})</small>
</span>
</div>
</div>
<div v-if="msg.content.relations?.length" class="kg-section">
<h5>识别出的关系</h5>
<ul class="relation-list">
<li v-for="(rel, i) in msg.content.relations" :key="i">
<span class="rel-subject">{{ rel.e1 }}</span>
<span class="rel-predicate"> {{ rel.r }} </span>
<span class="rel-object">{{ rel.e2 }}</span>
</li>
</ul>
</div>
<div v-if="!msg.content.entities?.length && !msg.content.relations?.length" class="empty-state">
未提取到有效医学实体或关系
</div>
</div>
<div v-else class="bubble assistant-text">
{{ msg.content }}
</div>
</div>
</div> </div>
</div> </div>
<!-- 输入区域Qwen 风格 --> <!-- 输入区域Qwen 风格 -->
@ -60,7 +99,7 @@
<script> <script>
import Menu from "@/components/Menu.vue"; import Menu from "@/components/Menu.vue";
import axios from "axios"; import axios from "axios";
import {analyze} from "@/api/builder"; import {analyze, getAIStatus} from "@/api/builder";
export default { export default {
name: 'QwenChat', name: 'QwenChat',
@ -69,28 +108,44 @@ export default {
return { return {
inputText: '', inputText: '',
messages: [ messages: [
{ role: 'assistant', content: '你好!我是 Qwen,有什么可以帮你的吗?' } { role: 'assistant', content: '你好!我是图谱构建助手,有什么可以帮你的吗?',isKG:false }
] ],
status:"wait"
}; };
}, },
methods: { methods: {
pollAIStatus() {
if (this.status !== "wait") return; //
try {
const data = getAIStatus();
if (data.status === "ok") {
this.status = "ok";
} else if (data.status === "error") {
this.status = "error";
} else {
setTimeout(() => this.pollAIStatus(), 3000);
}
} catch (error) {
console.error("轮询出错:", error);
this.status = "error";
alert("查询状态失败,请重试");
}
},
sendMessage() { sendMessage() {
const text = this.inputText.trim(); const text = this.inputText.trim();
if (!text) return; if (!text) return;
let data={ let data={
"text":text "text":text
} }
analyze(data) // this.pollAIStatus()
// //
this.messages.push({ role: 'user', content: text }); this.messages.push({ role: 'user', content: text });
this.inputText = ''; this.inputText = '';
// AI API analyze(data).then(res=>{
setTimeout(() => { console.log(res)
this.messages.push({ this.messages.push({ role: 'assistant',
role: 'assistant', content: res,entities:res.entities,relations:res.relations,isKG:true });
content: `你刚才说:“${text}” —— 这是一个模拟回复。` })
});
}, 600);
}, },
insertPrefix(prefix) { insertPrefix(prefix) {
this.inputText += prefix; this.inputText += prefix;
@ -125,7 +180,6 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
max-width: 800px;
margin: 0 auto; margin: 0 auto;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
@ -162,6 +216,7 @@ export default {
word-break: break-word; word-break: break-word;
line-height: 1.5; line-height: 1.5;
font-size: 14px; font-size: 14px;
text-align: left;
} }
.message.user .bubble { .message.user .bubble {
@ -273,4 +328,101 @@ export default {
.send-btn:hover { .send-btn:hover {
background: #8a53e0; background: #8a53e0;
} }
/* === 自定义助手 KG 卡片 === */
.kg-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 16px;
padding: 16px;
width: 100%;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
font-size: 14px;
color: #333;
}
.kg-card h4 {
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
color: #1a73e8;
font-weight: 600;
}
.kg-section {
margin-bottom: 16px;
}
.kg-section h5 {
margin: 0 0 8px 0;
color: #555;
font-size: 14px;
font-weight: 600;
}
/* 实体标签 */
.entity-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.entity-tag {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 20px;
font-size: 13px;
background: #f0f8ff;
color: #1e88e5;
border: 1px solid #bbdefb;
}
.entity-tag small {
margin-left: 6px;
opacity: 0.8;
font-weight: normal;
}
/* 按类型着色(可扩展) */
.tag-疾病 { background: #ffebee; color: #c62828; border-color: #ffcdd2; }
.tag-症状 { background: #e8f5e9; color: #2e7d32; border-color: #c8e6c9; }
.tag-检查 { background: #fff8e1; color: #ff8f00; border-color: #ffecb3; }
.tag-药物 { background: #f3e5f5; color: #7b1fa2; border-color: #ce93d8; }
/* 关系列表 */
.relation-list {
list-style: none;
padding: 0;
margin: 0;
}
.relation-list li {
padding: 6px 0;
border-bottom: 1px dashed #eee;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.relation-list li:last-child {
border-bottom: none;
}
.rel-subject { font-weight: 600; color: #1a237e; }
.rel-predicate { color: #d32f2f; font-weight: 500; }
.rel-object { font-weight: 600; color: #1b5e20; }
/* 空状态 */
.empty-state {
color: #999;
font-style: italic;
padding: 12px 0;
}
/* 普通助手文本(非 KG) */
.assistant-text {
background-color: #e9ecef;
color: #333;
border-bottom-left-radius: 4px;
}
</style> </style>

451
vue/src/system/GraphDemo.vue

@ -20,7 +20,7 @@
class="color-block" class="color-block"
@click="toggleCategory(item.key)" @click="toggleCategory(item.key)"
></div> ></div>
<span>{{ item.label }}</span> <span style=" font-size: 12px;color: rgb(0, 0, 0);font-weight: 600;">{{ item.label }}</span>
</div> </div>
</div> </div>
</header> </header>
@ -187,8 +187,12 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
this._graph.destroy() if (this._graph!=null){
this._graph = null; this._graph.stopLayout();
this.clearGraphState();
this._graph.destroy()
this._graph = null;
}
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
}, },
watch: { watch: {
@ -216,20 +220,23 @@ export default {
methods: { methods: {
buildCategoryIndex() { buildCategoryIndex() {
const index = {}; const index = {};
const nodes = this._graph.getNodeData() // if (this._graph!=null){
nodes.forEach(node => { const nodes = this._graph.getNodeData() //
console.log(node.data.label) nodes.forEach(node => {
const category = node.data.label; // label console.log(node.data.label)
if(category=='Drug'||category=='Symptom'|| const category = node.data.label; // label
category=='Disease'||category=='Check'){ if(category=='Drug'||category=='Symptom'||
if (!index[category]) index[category] = []; category=='Disease'||category=='Check'){
index[category].push(node.id); if (!index[category]) index[category] = [];
}else{ index[category].push(node.id);
if (!index["Other"]) index["Other"] = []; }else{
index["Other"].push(node.id); if (!index["Other"]) index["Other"] = [];
} index["Other"].push(node.id);
}); }
this.categoryToNodeIds = index; });
this.categoryToNodeIds = index;
}
}, },
// //
toggleCategory (key){ toggleCategory (key){
@ -433,207 +440,211 @@ export default {
console.log(this.defaultData) console.log(this.defaultData)
console.log(this._nodeLabelMap) console.log(this._nodeLabelMap)
const container = this.$refs.graphContainer; const container = this.$refs.graphContainer;
const width = container.clientWidth || 800; console.log(container)
const height = container.clientHeight || 600; if (container!=null){
console.log(width) const width = container.clientWidth || 800;
console.log(height) const height = container.clientHeight || 600;
const graph = new Graph({ console.log(width)
container, console.log(height)
width, const graph = new Graph({
height, container,
layout: { width,
type: 'force', // height,
gravity: 0.3, // layout: {
repulsion: 500, // type: 'force', //
attraction: 20, // gravity: 0.3, //
preventOverlap: true // repulsion: 500, //
// type: 'radial', attraction: 20, //
// preventOverlap: true, preventOverlap: true //
// unitRadius: 200, // type: 'radial',
// maxPreventOverlapIteration:100 // preventOverlap: true,
// type: 'force-atlas2', // unitRadius: 200,
// preventOverlap: true, // maxPreventOverlapIteration:100
// kr: 1000, // type: 'force-atlas2',
// center: [250, 250], // preventOverlap: true,
// barnesHut:true, // kr: 1000,
// center: [250, 250],
// barnesHut:true,
},
behaviors: [ 'zoom-canvas', 'drag-element',
'click-select','focus-element', {
type: 'hover-activate',
degree: 1,
},
{
type: 'drag-canvas',
enable: (event) => event.shiftKey === false,
}, },
{ behaviors: [ 'zoom-canvas', 'drag-element',
type: 'brush-select', 'click-select','focus-element', {
}, type: 'hover-activate',
], degree: 1,
node: {
style: {
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; type: 'drag-canvas',
if (label === 'Disease') return '#B91C1C'; enable: (event) => event.shiftKey === false,
if (label === 'Drug') return '#047857';
if (label === 'Check') return '#40999b'; //
if (label === 'Symptom') return '#B45309';
return '#1D4ED8';
}, },
labelText: (d) => d.data.name, {
labelPlacement: 'center', type: 'brush-select',
labelWordWrap: true,
labelMaxWidth: '150%',
labelMaxLines: 3,
labelTextOverflow: 'ellipsis',
labelTextAlign: 'center',
opacity: 1
},
state: {
active: {
lineWidth: 2,
shadowColor: '#ffffff',
shadowBlur: 10,
opacity: 1
}, },
inactive: { ],
opacity: 0.3
}, node: {
normal:{ style: {
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';
},
labelText: (d) => d.data.name,
labelPlacement: 'center',
labelWordWrap: true,
labelMaxWidth: '150%',
labelMaxLines: 3,
labelTextOverflow: 'ellipsis',
labelTextAlign: 'center',
opacity: 1 opacity: 1
} },
state: {
active: {
lineWidth: 2,
shadowColor: '#ffffff',
shadowBlur: 10,
opacity: 1
},
inactive: {
opacity: 0.3
},
normal:{
opacity: 1
}
},
},
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
}, },
// 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: { edge: {
selected: { style: {
stroke: '#1890FF', labelText: (d) => d.data.relationship.properties.label,
lineWidth: 2, stroke: (d) => {
}, // target label
highlight: { const targetLabel = this._nodeLabelMap.get(d.target); // d.target ID
halo: true, // target
haloStroke: '#1890FF', if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
haloLineWidth: 6, if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
haloStrokeOpacity: 0.3, if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
lineWidth: 3, if (targetLabel === 'Check') return 'rgba(89,209,212,0.5)'; //
opacity: 1 return 'rgba(51,110,238,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
// }
}, },
inactive: { state: {
opacity: 0.3 selected: {
stroke: '#1890FF',
lineWidth: 2,
},
highlight: {
halo: true,
haloStroke: '#1890FF',
haloLineWidth: 6,
haloStrokeOpacity: 0.3,
lineWidth: 3,
opacity: 1
},
inactive: {
opacity: 0.3
},
normal:{
opacity: 1
}
}, },
normal:{
opacity: 1
}
}, },
data:this.defaultData,
});
graph.render();
// graph.on('node:pointerover', (evt) => {
// const nodeItem = evt.target; //
// const relatedEdges = graph.getRelatedEdgesData(nodeItem.id);
// const relatedEdgeIds = relatedEdges.map(edge => edge.id);
//
// // 3. 'highlight'
// relatedEdgeIds.forEach(edgeId => {
// graph.setElementState(edgeId, 'highlight', true);
// });
// graph.setElementState(nodeItem.id, 'active',true);
//
// // 2. ID
// const neighborNodeIds = new Set();
// relatedEdges.forEach(edge => {
// if (edge.source !== nodeItem.id) neighborNodeIds.add(edge.source);
// if (edge.target !== nodeItem.id) neighborNodeIds.add(edge.target);
// });
//
// neighborNodeIds.forEach(id => {
// graph.setElementState(id, 'active', true);
// });
//
// graph.getEdgeData().forEach(edge => {
// if (!relatedEdgeIds.includes(edge.id)) {
// graph.setElementState(edge.id, 'inactive', true);
// }
// });
// graph.getNodeData().forEach(node => {
// if (node.id !== nodeItem.id && !neighborNodeIds.has(node.id)) {
// graph.setElementState(node.id, 'inactive',true);
// }
// });
// });
// graph.on('node:pointerleave', (evt) => {
// graph.getEdgeData().forEach(edge => {
// graph.setElementState(edge.id, 'highlight', false);
// graph.setElementState(edge.id, 'inactive', false);
// graph.setElementState(edge.id, 'normal', true);
// });
// graph.getNodeData().forEach(node => {
// graph.setElementState(node.id, 'active', false);
// graph.setElementState(node.id, 'inactive', false);
// graph.setElementState(node.id, 'normal', true);
// });
// });
graph.on('node:click', (evt) => {
const nodeItem = evt.target.id; //
let node=graph.getNodeData(nodeItem).data
let data={
label:node.name,
type:node.label
}
getGraph(data).then(response=>{
console.log(response)
this.formatData(response)
}); // Promise
});
this._graph = graph
this._graph?.fitView()
}
},
data:this.defaultData,
});
graph.render();
// graph.on('node:pointerover', (evt) => {
// const nodeItem = evt.target; //
// const relatedEdges = graph.getRelatedEdgesData(nodeItem.id);
// const relatedEdgeIds = relatedEdges.map(edge => edge.id);
//
// // 3. 'highlight'
// relatedEdgeIds.forEach(edgeId => {
// graph.setElementState(edgeId, 'highlight', true);
// });
// graph.setElementState(nodeItem.id, 'active',true);
//
// // 2. ID
// const neighborNodeIds = new Set();
// relatedEdges.forEach(edge => {
// if (edge.source !== nodeItem.id) neighborNodeIds.add(edge.source);
// if (edge.target !== nodeItem.id) neighborNodeIds.add(edge.target);
// });
//
// neighborNodeIds.forEach(id => {
// graph.setElementState(id, 'active', true);
// });
//
// graph.getEdgeData().forEach(edge => {
// if (!relatedEdgeIds.includes(edge.id)) {
// graph.setElementState(edge.id, 'inactive', true);
// }
// });
// graph.getNodeData().forEach(node => {
// if (node.id !== nodeItem.id && !neighborNodeIds.has(node.id)) {
// graph.setElementState(node.id, 'inactive',true);
// }
// });
// });
// graph.on('node:pointerleave', (evt) => {
// graph.getEdgeData().forEach(edge => {
// graph.setElementState(edge.id, 'highlight', false);
// graph.setElementState(edge.id, 'inactive', false);
// graph.setElementState(edge.id, 'normal', true);
// });
// graph.getNodeData().forEach(node => {
// graph.setElementState(node.id, 'active', false);
// graph.setElementState(node.id, 'inactive', false);
// graph.setElementState(node.id, 'normal', true);
// });
// });
graph.on('node:click', (evt) => {
const nodeItem = evt.target.id; //
let node=graph.getNodeData(nodeItem).data
let data={
label:node.name,
type:node.label
}
getGraph(data).then(response=>{
console.log(response)
this.formatData(response)
}); // Promise
});
this._graph = graph
this._graph?.fitView()
}, },
@ -850,15 +861,15 @@ button:hover {
.legend-item { .legend-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 7px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.color-block { .color-block {
width: 16px; width: 30px;
height: 16px; height: 15px;
border-radius: 4px; border-radius: 18px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@ -885,13 +896,14 @@ button:hover {
} }
.search-container { .search-container {
width: 260px; width: 330px;
height: 36px; height: 30px;
background: #f5f7fa; background: #fff;
border-radius: 18px; border-radius: 18px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 15px; padding: 0 15px;
box-shadow: 4px 2px 9px 1px rgb(97 99 100 / 19%);
} }
.search-container input { .search-container input {
@ -937,6 +949,7 @@ button:hover {
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
} }
.disease-header { .disease-header {
@ -945,12 +958,24 @@ button:hover {
color: #fff; color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
padding: 0 15px; padding: 0 15px;
justify-content: flex-start;
}
.d-title{
display: flex;
align-items: center;
font-size: 13px;
}
.d-count{
font-size: 9px;
background-color: #5989F0;
border-radius: 7px;
padding: 0px 4px;
margin-left: 7px;
} }
.d-icon { .d-icon {
width: 18px; width: 21px;
margin-right: 8px; margin-right: 8px;
} }
@ -970,6 +995,7 @@ button:hover {
align-items: center; align-items: center;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
font-weight: 500;
} }
.code { .code {
@ -985,6 +1011,7 @@ button:hover {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: left; text-align: left;
font-size: 12px;
} }
.disease-body{ .disease-body{
width: 360px; width: 360px;

85
vue/src/system/Login.vue

@ -2,7 +2,7 @@
<div class="login-container"> <div class="login-container">
<!-- 左上角Logo和标题 --> <!-- 左上角Logo和标题 -->
<div class="logo-header"> <div class="logo-header">
<img src="@/assets/logo.png" alt="Logo" class="logo"> <img src="../assets/logo.png" alt="Logo" class="logo">
<h1 class="login-title">面向疾病预测的知识图谱应用系统</h1> <h1 class="login-title">面向疾病预测的知识图谱应用系统</h1>
</div> </div>
@ -12,8 +12,9 @@
</div> </div>
<div class="login-form"> <div class="login-form">
<h2 class="form-title">登录</h2> <div class="form-title">登录</div>
<p class="form-description">请输入您的电子邮件地址和密码以访问账户</p> <div class="form-description">请输入您的电子邮件地址和密码以访问账户</div>
<form class="form" @submit.prevent="handleLogin"> <form class="form" @submit.prevent="handleLogin">
<!-- 错误信息显示 --> <!-- 错误信息显示 -->
@ -35,7 +36,7 @@
<div class="form-group"> <div class="form-group">
<div class="password-header"> <div class="password-header">
<label for="password" class="form-label">密码</label> <label for="password" class="form-label">密码</label>
<a href="#" class="forgot-password">忘记密码?</a> <!-- <a href="#" class="forgot-password">忘记密码?</a>-->
</div> </div>
<input <input
type="password" type="password"
@ -67,9 +68,9 @@
</button> </button>
</form> </form>
<div class="social-login"> <!-- <div class="social-login">-->
<p class="social-text">使用其他方式登录</p> <!-- <p class="social-text">使用其他方式登录</p>-->
</div> <!-- </div>-->
<div class="register-link"> <div class="register-link">
<p>还没有账户? <a href="#" class="register"> 立即注册</a></p> <p>还没有账户? <a href="#" class="register"> 立即注册</a></p>
@ -170,7 +171,6 @@ body, html {
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
flex-direction: row; flex-direction: row;
font-family: 'SimSun', '宋体', serif;
} }
/* 左上角Logo和标题样式 */ /* 左上角Logo和标题样式 */
@ -191,8 +191,7 @@ body, html {
.login-title { .login-title {
font-size: 17px; font-size: 17px;
font-weight: 900; font-weight: 600;
font-family: 'SimSun Bold', '宋体', serif;
color: #1f2937; color: #1f2937;
margin: 0; margin: 0;
white-space: nowrap; white-space: nowrap;
@ -212,10 +211,8 @@ body, html {
} }
.login-header { .login-header {
margin-bottom: 1.5rem;
width: 100%; width: 100%;
max-width: 24rem; max-width: 24rem;
margin-top: 20px;
} }
.login-form { .login-form {
@ -226,24 +223,19 @@ body, html {
.form-title { .form-title {
font-size: 18px; font-size: 18px;
font-weight: 900; font-weight: 600;
color: #333333; color: #333333;
margin-top: -7px; margin-bottom: 6px;
margin-bottom: 10px;
margin-left: 13px;
text-shadow: 0.2px 0.2px 0 #1f2937; text-shadow: 0.2px 0.2px 0 #1f2937;
text-align: left; text-align: left;
font-family: 'SimSun', '宋体', serif;
} }
.form-description { .form-description {
color: #B5B5B5; color: #A3A3A3;
margin-bottom: 2rem; margin-bottom: 2rem;
margin-left: 13px;
text-align: left; text-align: left;
font-size: 11px; font-size: 11px;
font-weight: bold; font-weight: bold;
font-family: 'SimSun', '宋体', serif;
} }
.form { .form {
@ -272,24 +264,24 @@ body, html {
.form-label { .form-label {
display: block; display: block;
font-size: 11px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: #374151; color: #757575;
margin-bottom: 0.3rem; margin-bottom: 8px;
text-align: left; text-align: left;
font-family:'STSong', '宋体', serif;
} }
.form-input { .form-input {
width: 100%; width: 100%;
padding: 0.6rem 0.8rem; padding: 0.4rem 0.7rem;
border-radius: 0.5rem; border-radius: 7px;
border: 2px solid #A3A3A3; border: 2px solid #A3A3A3;
transition: all 0.2s; transition: all 0.2s;
font-size: 9px; font-size: 11px;
box-sizing: border-box; box-sizing: border-box;
font-family: 'SimSun', '宋体', serif;
background-color: #FFFFFF; background-color: #FFFFFF;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
font-weight: 600;
} }
.form-input:focus { .form-input:focus {
@ -310,9 +302,8 @@ body, html {
.forgot-password { .forgot-password {
font-size: 9px; font-size: 9px;
color: #B5B5B5; color: #A3A3A3;
text-decoration: none; text-decoration: none;
font-family: 'SimSun', '宋体', serif;
} }
.forgot-password:hover { .forgot-password:hover {
@ -339,17 +330,15 @@ body, html {
font-size: 11px; font-size: 11px;
color: #444040ba; color: #444040ba;
font-weight: bold; font-weight: bold;
font-family: 'SimSun', '宋体', serif;
} }
.login-button { .login-button {
width: 100%; width: 100%;
background-color: #409EFF; background-color: #175EFF;
color: white; color: white;
font-weight: 500; font-size: 12px;
font-size: 11px; padding: 0.4rem 0.8rem;
padding: 0.6rem 0.8rem; border-radius: 3px;
border-radius: 0;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
@ -357,14 +346,14 @@ body, html {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
font-family: 'SimSun', '宋体', 'STSong', '华文宋体', serif;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.2); box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.2);
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
} }
.login-icon { .login-icon {
height: 0.9rem; height: 14px;
width: auto; width: auto;
margin-right: 8px; margin-right: 6px;
} }
.login-button:hover { .login-button:hover {
@ -380,7 +369,6 @@ body, html {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 1.5rem 0; margin: 1.5rem 0;
font-family: 'SimSun', '宋体', serif;
} }
.divider::before, .divider::before,
@ -394,8 +382,7 @@ body, html {
.divider span { .divider span {
padding: 0 1rem; padding: 0 1rem;
font-size: 11px; font-size: 11px;
color: #B5B5B5; color: #A3A3A3;
font-family: 'SimSun', '宋体', serif;
} }
.social-login { .social-login {
@ -404,11 +391,10 @@ body, html {
.social-text { .social-text {
text-align: center; text-align: center;
color: #B5B5B5; color: #A3A3A3;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 11px; font-size: 11px;
font-weight: bold; font-weight: bold;
font-family: 'SimSun', '宋体', serif;
} }
.social-icons { .social-icons {
@ -445,18 +431,14 @@ body, html {
} }
.register-link p { .register-link p {
color: #B5B5B5; color: #A3A3A3;
font-size: 11px; font-size: 11px;
font-weight: bold; font-weight: bold;
font-family: 'SimSun', '宋体', serif;
} }
.register { .register {
color: #B5B5B5; color: #A3A3A3;
font-weight: 500;
text-decoration: none; text-decoration: none;
font-weight: bold;
font-family: 'SimSun', '宋体', serif;
} }
.register:hover { .register:hover {
@ -511,7 +493,6 @@ body, html {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
font-family: 'SimSun', '宋体', serif;
} }
.graph-wrapper { .graph-wrapper {
@ -533,7 +514,7 @@ body, html {
} }
.login-form-container { .login-form-container {
padding: 2rem; padding: 1.5rem;
} }
.graph-container { .graph-container {

3
vue/src/system/Profile.vue

@ -1,8 +1,7 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 引入侧边栏组件 --> <!-- 引入侧边栏组件 -->
<Menu <Menu
:initial-active="0"
@menu-click="handleSidebarClick" @menu-click="handleSidebarClick"
/> />

2
vue/src/utils/request.js

@ -2,7 +2,7 @@ import axios from 'axios';
const service = axios.create({ const service = axios.create({
baseURL: '/', baseURL: '/',
timeout: 10000, timeout: 18000000,
}); });
service.interceptors.request.use( service.interceptors.request.use(

Loading…
Cancel
Save