Browse Source

all

hanyuqing
hanyuqing 3 months ago
parent
commit
1509281493
  1. 12
      controller/QAController.py
  2. 4
      controller/client.py
  3. 3
      util/auth_interceptor.py
  4. 3
      vue/src/api/profile.js
  5. 55
      vue/src/components/Menu.vue
  6. 49
      vue/src/system/GraphBuilder.vue
  7. 64
      vue/src/system/GraphDemo.vue
  8. 63
      vue/src/system/GraphQA.vue
  9. 4
      vue/src/system/KGData.vue
  10. 20
      vue/src/system/Login.vue
  11. 75
      vue/src/system/Register.vue

12
controller/QAController.py

@ -9,6 +9,8 @@ from robyn import jsonify, Response
from app import app
from controller.client import client
import uuid
def convert_to_g6_format(data):
entities = data["entities"]
relations = data["relations"]
@ -25,13 +27,12 @@ def convert_to_g6_format(data):
nodes.append({
"id": node_id,
"label": name,
"data":{
"data": {
"type": ent["t"] # 可用于 G6 的节点样式区分
}
})
# 构建边
# 构建边,并为每条边生成唯一 ID
edges = []
for rel in relations:
e1 = rel["e1"]
@ -42,11 +43,14 @@ def convert_to_g6_format(data):
target_id = name_to_id.get(e2)
if source_id and target_id:
edge_id = str(uuid.uuid4()) # 👈 为边生成唯一 ID
edges.append({
"id": edge_id, # ✅ 添加 id 字段
"source": source_id,
"target": target_id,
"label": r, # G6 支持直接使用 label(非必须放 data)
"data": {
"label": r
"label": r # 保留 data.label 便于扩展
}
})
else:

4
controller/client.py

@ -1,3 +1,5 @@
import httpx
client = httpx.AsyncClient(base_url="http://192.168.50.113:8088")
client = httpx.AsyncClient(base_url="http://192.168.50.113:8088")
# client = httpx.AsyncClient(base_url="http://192.168.2.20:8088")

3
util/auth_interceptor.py

@ -106,7 +106,8 @@ def global_auth_interceptor(request):
"""全局认证拦截器 - 用于 @app.before_request() 装饰器"""
# 获取请求路径
path = str(request.url.path) if hasattr(request.url, 'path') else str(request.url)
print(path)
print("1111111111111111=============")
# 公开路径直接放行
if is_public_path(path):
return request

3
vue/src/api/profile.js

@ -6,11 +6,10 @@ import request from '@/utils/request';
* 后端接口GET /api/userInfo
* @param {string} token - 用户登录令牌
*/
export function getUserProfile(token) {
export function getUserProfile() {
return request({
url: '/api/userInfo',
method: 'get',
});
}

55
vue/src/components/Menu.vue

@ -80,7 +80,7 @@ const props = defineProps({
//
const emit = defineEmits(['menu-click']);
const userProfile = ref({
let userProfile = ref(JSON.parse(localStorage.getItem('userProfile')) || {
username: '用户',
avatar: '/resource/avatar/用户.png'
});
@ -111,39 +111,36 @@ const handleProfile = () => {
const handleLogout = () => {
localStorage.removeItem('messages');
localStorage.removeItem('token'); // 退token
localStorage.removeItem('userProfile'); //
router.push('/login');
};
onMounted(async () => {
try {
const token = localStorage.getItem('token');
console.log('Profile组件挂载,获取到的token:', token);
if (token) {
const response = await getUserProfile(token);
if (response.success) {
let avatarUrl = response.user.avatar || '/resource/avatar/4.png';
//
userProfile.value = {
username: response.user.username,
avatar: avatarUrl
};
} else {
// token
if (response.message && response.message.includes('登录')) {
localStorage.removeItem('token');
ElMessage.warning('登录已过期,请重新登录');
}
}
} else {
console.log('用户未登录');
// ElMessage errorMessage.value
ElMessage.info('您当前处于游客模式,请登录后操作');
// localStorage
const cached = localStorage.getItem('userProfile');
if (cached) {
try {
userProfile.value = JSON.parse(cached);
return; //
} catch (e) {
console.warn('userProfile 缓存解析失败,将重新请求');
}
} catch (error) {
console.error('获取用户信息失败:', error);
// ElMessage
ElMessage.error('无法连接到服务器,请检查网络');
}
//
const response = await getUserProfile();
if (response?.success && response.user) {
const avatarUrl = response.user.avatar || '/resource/avatar/4.png';
userProfile.value = {
username: response.user.username,
avatar: avatarUrl
};
// localStorage
localStorage.setItem('userProfile', JSON.stringify(userProfile.value));
} else {
//
ElMessage.warning('用户信息加载失败,使用默认头像');
}
});

49
vue/src/system/GraphBuilder.vue

@ -5,6 +5,7 @@
:initial-active="1"
/>
<div class="qwen-chat">
<!-- 聊天消息区域 -->
<div ref="messagesContainer" class="chat-messages">
<div
@ -103,8 +104,13 @@
</div>
</div>
<div v-if="isSending">
<img src="../assets/load.gif" style="width: 100px;">
<div v-if="isSending" style="display: flex;margin-left: 5px;margin-bottom: 10px">
<div class="loading-indicator">
<div class="dot dot-1"></div>
<div class="dot dot-2"></div>
<div class="dot dot-3"></div>
<div class="dot dot-4"></div>
</div>
</div>
</div>
<!-- 输入区域Qwen 风格 -->
@ -858,4 +864,43 @@ export default {
cursor: not-allowed;
background-color: #ccc;
}
/* 加载指示器的整体样式 */
.loading-indicator {
display: inline-flex;
align-items: center;
gap: 10px; /* 圆点之间的间距 */
}
/* 单个圆点的基本样式 */
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
/* 给每个圆点设定特定的灰色 */
.dot-1 { background-color: #e0e0e0; } /* 浅灰色 */
.dot-2 { background-color: #b0b0b0; } /* 中浅灰色 */
.dot-3 { background-color: #808080; } /* 中深灰色 */
.dot-4 { background-color: #505050; } /* 深灰色 */
/* 给每个圆点设置不同的动画延迟以创造连续的效果 */
.dot:nth-child(1) { animation-delay: 0.1s; }
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.3s; }
.dot:nth-child(4) { animation-delay: 0.4s; }
.dot-1 { animation-delay: -0.4s; }
.dot-2 { animation-delay: -0.2s; }
.dot-3 { animation-delay: 0s; }
.dot-4 { animation-delay: 0.2s; }
/* 定义动画效果 */
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
} 40% {
transform: scale(1.0);
}
}
</style>

64
vue/src/system/GraphDemo.vue

@ -59,7 +59,6 @@
:props="treeProps"
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
@ -234,7 +233,6 @@ export default {
labelFontFamily: this.edgeFontFamily,
labelFill: this.edgeFontColor,
},
}))
const updatedData = {
nodes: updatedNodes,
@ -364,6 +362,7 @@ export default {
changeTree(){
if(this.typeRadio=="Disease"){
this.treeData=this.diseaseTree
}
if(this.typeRadio=="Drug") {
this.treeData=this.drugTree
@ -372,6 +371,34 @@ export default {
this.treeData=this.checkTree
}
},
loadTreeNode(node, resolve, data) {
//
let apiCall;
if (this.typeRadio === 'Disease') {
// ICD-10 level chapter, section
// code id
apiCall = () => this.loadDiseaseTreeData();
} else if (this.typeRadio === 'Drug') {
apiCall = () => this.loadDrugTreeData(); // API
} else if (this.typeRadio === 'Check') {
apiCall = () => this.loadCheckTreeData();
}
if (apiCall) {
apiCall()
.then(children => {
// children labelcode/id
// []
resolve(children || []);
})
.catch(err => {
console.error('加载子节点失败:', err);
resolve([]); //
});
} else {
resolve([]);
}
},
async loadDiseaseTreeData() {
try {
const res = await fetch('/icd10.json')
@ -507,7 +534,7 @@ export default {
console.log(this.defaultData)
console.log(this._nodeLabelMap)
const container = this.$refs.graphContainer;
console.log(container)
if (!container) return;
if (container!=null){
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
@ -522,7 +549,8 @@ export default {
gravity: 0.3, //
repulsion: 500, //
attraction: 20, //
preventOverlap: true //
preventOverlap: true, //
// center: [width / 2, height / 2]
// type: 'radial',
// preventOverlap: true,
// unitRadius: 200,
@ -644,8 +672,18 @@ export default {
});
console.log("Container width:", container.clientWidth);
console.log("Container height:", container.clientHeight);
console.log("Container scroll position before:", container.scrollTop, container.scrollLeft);
this.$nextTick(() => {
graph.render();
// graph.fitView()
graph.fitView({ padding: 30, duration: 1000, easing: 'ease-in' });
});
graph.render();
console.log("Container scroll position after:", container.scrollTop, container.scrollLeft);
// graph.fitView();
// graph.on('node:pointerover', (evt) => {
// const nodeItem = evt.target; //
// const relatedEdges = graph.getRelatedEdgesData(nodeItem.id);
@ -703,13 +741,13 @@ export default {
this.formatData(response)
}); // Promise
});
graph.once('afterlayout', () => {
if (!graph.destroyed) {
graph.fitCenter({ padding: 40, duration: 1000 });
}
});
this._graph = graph
this._graph?.fitView()
}
@ -717,10 +755,8 @@ export default {
updateGraph(data) {
if (!this._graph) return
this._graph.setData(data)
this._graph.render()
this._graph.render();
},
updateAllNodes() {

63
vue/src/system/GraphQA.vue

@ -37,6 +37,14 @@
</div>
</div>
<h2 class="section-title">问答结果</h2>
<div v-if="isSending">
<div class="loading-dots">
<div class="dot dot-1"></div>
<div class="dot dot-2"></div>
<div class="dot dot-3"></div>
<div class="dot dot-4"></div>
</div>
</div>
<div v-if="answers.length === 0" class="empty-state">
请输入问题进行查询...
</div>
@ -100,7 +108,8 @@ export default {
edgeFontFamily: 'Microsoft YaHei, sans-serif',
queryRecord:""
queryRecord:"",
isSending:false
};
},
@ -115,13 +124,13 @@ export default {
// =============== 👇 localStorage ===============
this.restoreDataFromLocalStorage();
// =======================================================================
// this.answers=[]
//
if (this.answers.length > 0) {
this.initGraph(this.answers[0].result);
// console.log(this.answers[0].result)
}
},
beforeUnmount() {
// =============== 👇==============
this.saveDataToLocalStorage();
@ -189,11 +198,17 @@ export default {
},
selectGraph(index){
this.selected=index
this.formatData(this.answers[index].result)
if(this.answers.length>0){
this.formatData(this.answers[index].result)
}
},
handleSearch(){
this.isSending=true
this.answers=[]
this.formatData([])
if (this._graph){
this._graph.clear()
}
let data={
text:this.query
}
@ -208,6 +223,7 @@ export default {
if(this.answers.length>0){
this.initGraph(this.answers[0].result)
}
this.isSending=false
})
},
formatData(data){
@ -249,6 +265,7 @@ export default {
},
updateGraph(data) {
if (!this._graph) return
this._graph.setData(data)
this._graph.render()
},
@ -586,5 +603,41 @@ export default {
border: 1px solid #165DFF;
border-radius: 8px;
}
/* 加载动画容器 */
.loading-dots {
display: flex;
justify-content: center;
align-items: center;
gap: 10px; /* 圆点之间的间距 */
}
/* 每个圆点的基础样式 */
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #a6d2ff; /* 默认浅蓝 */
animation: bounce 1.2s infinite ease-in-out both;
}
/* 使用不同的蓝色调 */
.dot-1 { background-color: #4096ff; } /* 较深的蓝色 */
.dot-2 { background-color: #6fbfff; } /* 中等蓝色 */
.dot-3 { background-color: #8ed1ff; } /* 浅一些的蓝色 */
.dot-4 { background-color: #a6d2ff; } /* 更浅的蓝色 */
/* 定义弹跳动画 */
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
} 40% {
transform: scale(1.0);
}
}
/* 让每个圆点的动画延迟出现,形成连续效果 */
.dot-1 { animation-delay: -0.4s; }
.dot-2 { animation-delay: -0.2s; }
.dot-3 { animation-delay: 0s; }
.dot-4 { animation-delay: 0.2s; }
</style>

4
vue/src/system/KGData.vue

@ -97,7 +97,7 @@
<div class="op-group">
<el-button class="ref-op-btn edit" @click="openNodeDialog(scope.row)">编辑</el-button>
<el-button class="ref-op-btn delete" @click="handleDelete(scope.row, 'node')">删除</el-button>
<el-button class="ref-op-btn view" @click="handleView(scope.row, 'node')">详情</el-button>
<!-- <el-button class="ref-op-btn view" @click="handleView(scope.row, 'node')">详情</el-button>-->
</div>
</template>
</el-table-column>
@ -601,7 +601,7 @@ onMounted(() => {
.custom-folder-tabs { display: flex; padding-left: 40px; }
.folder-tab-item { padding: 8px 20px; font-size: 12px; color: #86909c; cursor: pointer; background-color: #ecf2ff; border: 1px solid #dcdfe6; border-bottom: none; border-radius: 8px 8px 0 0; }
.folder-tab-item.active { background-color: #f1f6ff !important; color: #2869ff; font-weight: bold; border: 2px solid #6896ff; border-bottom: 2px solid #ffffff; margin-bottom: -1px; z-index: 3; }
.data-card-container { background: #ffffff; border-radius: 30px; padding: 20px 20px; box-shadow: 2px -1px 14px 4px #E1EAFF; border: 1px solid #eff4ff; position: relative; z-index: 4; min-height: 380px; }
.data-card-container { background: #ffffff; border-radius: 30px; padding: 20px 20px; box-shadow: 2px -1px 14px 4px #E1EAFF; border: 1px solid #eff4ff; position: relative; z-index: 4; }
.filter-bar { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 20px; }
.filter-inputs { display: flex; gap: 35px; flex-wrap: nowrap;margin-right: 20px; }
.input-group-inline { display: flex; align-items: center; gap: 12px; flex-shrink: 0; white-space: nowrap; }

20
vue/src/system/Login.vue

@ -30,6 +30,7 @@
v-model="loginForm.username"
placeholder="输入您的用户名"
class="form-input"
autocomplete="username"
>
</div>
@ -44,18 +45,19 @@
v-model="loginForm.password"
placeholder="输入您的密码"
class="form-input"
autocomplete="current-password"
>
</div>
<div class="form-checkbox">
<input
type="checkbox"
id="remember"
v-model="loginForm.remember"
class="checkbox"
>
<label for="remember" class="checkbox-label">记住密码</label>
</div>
<!-- <div class="form-checkbox">-->
<!-- <input -->
<!-- type="checkbox" -->
<!-- id="remember" -->
<!-- v-model="loginForm.remember"-->
<!-- class="checkbox"-->
<!-- >-->
<!-- <label for="remember" class="checkbox-label">记住密码</label>-->
<!-- </div>-->
<button
type="submit"

75
vue/src/system/Register.vue

@ -8,12 +8,9 @@
<!-- 左侧注册区域 -->
<div class="register-form-container">
<div class="register-header">
</div>
<div class="register-form">
<h2 class="form-title">注册</h2>
<p class="form-description">创建您的账户以访问系统功能</p>
<div class="form-title">注册</div>
<div class="form-description">创建您的账户以访问系统功能</div>
<form class="form" @submit.prevent="handleRegister">
<!-- 错误信息显示 -->
@ -87,15 +84,15 @@
<p v-if="errors.confirmPassword" class="error-text">{{ errors.confirmPassword }}</p>
</div>
<div class="form-checkbox">
<input
type="checkbox"
id="agree"
v-model="registerForm.agree"
class="checkbox"
>
<label for="agree" class="checkbox-label">我同意服务条款和隐私政策</label>
</div>
<!-- <div class="form-checkbox">-->
<!-- <input -->
<!-- type="checkbox" -->
<!-- id="agree" -->
<!-- v-model="registerForm.agree"-->
<!-- class="checkbox"-->
<!-- >-->
<!-- <label for="agree" class="checkbox-label">我同意服务条款和隐私政策</label>-->
<!-- </div>-->
<button
type="submit"
@ -106,10 +103,7 @@
<span v-else>注册中...</span>
</button>
</form>
<div class="social-login">
<p class="social-text">使用其他方式注册</p>
</div>
<div class="login-link">
<p>已有账户? <a href="#" class="login" @click.prevent="goToLogin"> 立即登录</a></p>
@ -140,7 +134,7 @@ const registerForm = ref({
username: '',
password: '',
confirmPassword: '',
agree: false,
agree: true,
avatar: null
});
@ -303,7 +297,6 @@ body, html {
height: 100vh;
overflow: hidden;
flex-direction: row;
font-family: 'SimSun', '宋体', serif;
}
/* 左上角Logo和标题样式 */
@ -325,7 +318,6 @@ body, html {
.register-title {
font-size: 17px;
font-weight: 900;
font-family: 'SimSun Bold', '宋体', serif;
color: #1f2937;
margin: 0;
white-space: nowrap;
@ -414,6 +406,7 @@ body, html {
opacity: 0;
transition: opacity 0.2s;
cursor: pointer;
border: 3px solid #e5e7eb;
}
.avatar-container:hover .avatar-overlay {
@ -479,15 +472,18 @@ body, html {
margin-bottom: 8px;
text-align: left;
}
.form-input {
width: 100%;
padding: 0.6rem 0.8rem;
border-radius: 0.5rem;
padding: 0.4rem 0.7rem;
border-radius: 7px;
border: 2px solid #A3A3A3;
transition: all 0.2s;
font-size: 9px;
font-size: 11px;
box-sizing: border-box;
background-color: #FFFFFF;
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
font-weight: 600;
}
.form-input:focus {
@ -542,14 +538,13 @@ body, html {
font-weight: bold;
}
.register-button {
.register-button{
width: 100%;
background-color: #409EFF;
background-color: #175EFF;
color: white;
font-weight: 500;
font-size: 11px;
padding: 0.6rem 0.8rem;
border-radius: 0;
font-size: 12px;
padding: 0.4rem 0.8rem;
border-radius: 3px;
border: none;
cursor: pointer;
transition: background-color 0.2s;
@ -558,6 +553,7 @@ body, html {
justify-content: center;
box-sizing: border-box;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.2);
font-family: 'Noto Serif SC', "SimSun", "宋体", serif;
}
.login-icon {
@ -645,25 +641,19 @@ body, html {
text-align: center;
}
.login-link p {
color: #B5B5B5;
font-size: 11px;
font-weight: bold;
font-family: 'SimSun', '宋体', serif;
}
.login {
color: #B5B5B5;
font-weight: 500;
color: #A3A3A3;
text-decoration: none;
font-weight: bold;
font-family: 'SimSun', '宋体', serif;
}
.login-link p {
color: #A3A3A3;
font-size: 11px;
font-weight: bold;
}
.login:hover {
color: #1d4ed8;
}
/* 右侧知识图谱可视化区域样式 */
.graph-container {
width: 75%;
@ -712,7 +702,6 @@ body, html {
align-items: center;
justify-content: center;
height: 100%;
font-family: 'SimSun', '宋体', serif;
}
.graph-wrapper {

Loading…
Cancel
Save