Browse Source

all

hanyuqing
hanyuqing 3 months ago
parent
commit
036550f5a5
  1. 10
      controller/GraphController.py
  2. 7
      vue/src/api/graph.js
  3. 15
      vue/src/components/Menu.vue
  4. 6
      vue/src/system/GraphDemo.vue
  5. 35
      vue/src/system/KGData.vue
  6. 127
      vue/src/system/Profile.vue

10
controller/GraphController.py

@ -43,7 +43,7 @@ def preload_data():
children = [{"label": name, "type": "Drug"} for name in sorted(groups[key])]
tree_data.append({"label": key, "type": "Drug", "children": children})
redis_set(DRUG_TREE_KEY, json.dumps(tree_data, ensure_ascii=False), ex=3600)
redis_set(DRUG_TREE_KEY, json.dumps(tree_data, ensure_ascii=False), ex=None)
# --- Check Tree ---
names = get_check_names_from_neo4j()
@ -58,7 +58,7 @@ def preload_data():
children = [{"label": name, "type": "Check"} for name in sorted(groups[key])]
tree_data.append({"label": key, "type": "Check", "children": children})
redis_set(CHECK_TREE_KEY, json.dumps(tree_data, ensure_ascii=False), ex=3600)
redis_set(CHECK_TREE_KEY, json.dumps(tree_data, ensure_ascii=False), ex=None)
print("✅ 预加载完成!数据已写入 Redis 缓存。")
except Exception as e:
@ -68,6 +68,12 @@ def preload_data():
# 执行预加载(在 app 创建前)
preload_data()
@app.get("/api/preload")
def preload():
preload_data()
@app.get("/api/getData")
def get_data():
try:

7
vue/src/api/graph.js

@ -13,6 +13,13 @@ export function getTestGraphData() {
});
}
export function preload() {
return request({
url: '/api/preload',
method: 'get'
});
}
export function getCount() {
return request({
url: '/api/getCount',

15
vue/src/components/Menu.vue

@ -57,7 +57,7 @@
</template>
<script setup>
import { onMounted, ref } from 'vue';
import {onMounted, ref, watch} from 'vue';
import { useRouter } from 'vue-router';
//
import { ElMessage } from 'element-plus';
@ -74,9 +74,15 @@ const props = defineProps({
initialActive: {
type: Number,
default: 0
},
avatar: {
type: String,
default: ''
}
});
// props.avatar userProfile localStorage
//
const emit = defineEmits(['menu-click']);
@ -114,7 +120,12 @@ const handleLogout = () => {
localStorage.removeItem('userProfile'); //
router.push('/login');
};
watch(() => props.avatar, (newAvatar) => {
if (newAvatar) {
userProfile.value.avatar = newAvatar;
localStorage.setItem('userProfile', JSON.stringify(userProfile.value));
}
}, { immediate: true });
onMounted(async () => {
// localStorage
const cached = localStorage.getItem('userProfile');

6
vue/src/system/GraphDemo.vue

@ -60,6 +60,7 @@
:data="treeData"
:props="treeProps"
accordion
expand-on-click-node
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
@ -598,7 +599,10 @@ export default {
async handleNodeClick(data) {
console.log('点击节点:', data)
// code
if(data.type=="Drug"||data.type=="Check"){
if (
(data.type === "Drug" || data.type === "Check") &&
!data.children // 确保没有 children 属性(或 children 为 undefined / null / 空数组
) {
const response = await getGraph(data); // Promise
this.formatData(response)
}

35
vue/src/system/KGData.vue

@ -79,7 +79,9 @@
</div>
<div class="filter-btns">
<el-button type="primary" class="btn-search-ref" @click="handleNodeSearch">搜索</el-button>
<el-button class="btn-orange" @click="openNodeDialog(null)">+ 新增节点</el-button>
<el-button type="primary" class="btn-import" @click="">导入</el-button>
<el-button type="primary" class="btn-export" @click="">导出</el-button>
<el-button class="btn-orange" @click="openNodeDialog(null)">新增节点</el-button>
</div>
</div>
@ -167,7 +169,7 @@
</div>
<div class="filter-btns">
<el-button type="primary" class="btn-search-ref" @click="handleRelSearch">查询</el-button>
<el-button class="btn-orange" @click="openRelDialog(null)">+ 新增关系</el-button>
<el-button class="btn-orange" @click="openRelDialog(null)">新增关系</el-button>
</div>
</div>
@ -279,9 +281,9 @@
/>
</el-select>
</el-form-item>
<el-form-item label="显示名称">
<el-input v-model="relForm.label" placeholder="用于前端展示的中文名称"/>
</el-form-item>
<!-- <el-form-item label="显示名称">-->
<!-- <el-input v-model="relForm.label" placeholder="用于前端展示的中文名称"/>-->
<!-- </el-form-item>-->
</el-form>
<template #footer>
<div class="dialog-footer-wrap">
@ -304,6 +306,7 @@ import {
addNode, updateNode, addRelationship, updateRelationship, deleteNode, deleteRelationship,
fixNodeIds
} from '@/api/data'
import {preload} from "@/api/graph";
// --- () ---
const CHINESE_TO_ENGLISH_LABEL = {
@ -497,6 +500,7 @@ const submitNode = async () => {
fetchNodes();
fetchStats();
fetchAllMetadata(); //
preload();
} else {
ElMessage.error(res?.msg || '操作失败');
}
@ -546,10 +550,15 @@ const submitRel = async () => {
};
const handleDelete = (row, type) => {
ElMessageBox.confirm('确认删除此项数据?', '警告', {type: 'warning'}).then(async () => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
customClass: 'my-custom-btns', //
confirmButtonText: '确认',
cancelButtonText: '取消'
}).then(async () => {
const res = type === 'node' ? await deleteNode(row.id) : await deleteRelationship(row.id);
if (res?.code === 200) {
ElMessage.success('删除成功');
preload();
type === 'node' ? fetchNodes() : fetchRels();
fetchStats();
fetchAllMetadata();
@ -625,6 +634,9 @@ onMounted(() => {
:deep(.el-form-item__content){box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px; }
.label-select {box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px; }
.btn-search-ref { background: #165dff !important; border-radius: 8px; height: 38px; }
.btn-import { background: rgb(145, 204, 117) !important; border-radius: 8px; height: 38px;border: none }
.btn-export { background: rgb(89, 209, 212) !important; border-radius: 8px; height: 38px;border: none }
.btn-orange { background: #ffb142 !important; color: white !important; border-radius: 8px; height: 38px; border: none !important; }
.table-compact { box-shadow: 0 4px 20px rgba(22, 93, 255, 0.08); overflow: hidden; }
.ref-table :deep(.el-table__header) th { background-color: #e8f0ff !important; color: #2869ff; font-weight: 700; }
@ -635,7 +647,7 @@ onMounted(() => {
.ref-op-btn.view { background-color: #ffb142 !important; }
.pagination-footer { margin-top: 20px; display: flex; justify-content: flex-end; }
:deep(.bold-header) { margin-right: 0 !important; display: flex !important; justify-content: flex-start !important; }
:deep(.bold-header .el-dialog__title) { font-family: "Microsoft YaHei", sans-serif !important; font-weight: 900 !important;padding:5px;margin-bottom: 10px; font-size: 19px !important;}
:deep(.bold-header .el-dialog__title) { color: #303133 !important;font-weight: 900 !important;padding:5px;margin-bottom: 10px; font-size: 19px !important;}
.custom-form :deep(.el-form-item__label) { color: #606266 !important;font-size: 16px !important; }
.custom-form :deep(.el-input) {box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px;}
.custom-form :deep(.el-select__wrapper){
@ -662,9 +674,7 @@ onMounted(() => {
color: #fff !important;
}
:deep(.el-dialog){border-radius: 7px}
:deep(.el-pager li:not(.is-active):hover) {
color: #165DFF !important;
}
:deep(.el-input__inner) {color:#606266}
:deep(.el-pagination .btn-next:hover){color: #165DFF !important;}
:deep(.el-pagination .btn-prev:hover){color: #165DFF !important;}
@ -677,4 +687,9 @@ onMounted(() => {
color: #165DFF !important;
font-weight: bold;
}
.my-custom-btns .el-button:not(.el-button--primary){ background-color: #e6e6e6 !important; border: none !important; color: #444 !important; padding: 18px 20px !important; font-weight: 500; }
.my-custom-btns .el-button--primary { background-color: #165dff !important; border: none !important; padding: 18px 20px !important; font-weight: 500; }
</style>

127
vue/src/system/Profile.vue

@ -3,6 +3,7 @@
<!-- 引入侧边栏组件 -->
<Menu
@menu-click="handleSidebarClick"
:avatar="userProfile.avatar"
/>
<!-- 主内容区域 -->
@ -33,11 +34,6 @@
>
</div>
<div class="username-display">{{ userProfile.username }}</div>
<div class="avatar-status">
<p v-if="avatarUploading" class="uploading-text">上传中...</p>
<p v-if="avatarUploadSuccess" class="success-text">头像更新成功</p>
</div>
</div>
<!-- 个人信息卡片 -->
<div class="profile-card">
@ -73,6 +69,9 @@
required style=" width: 97%;"
>
<div class="form-label" style="text-align: left;margin-top: 5px;font-size: 13px;font-weight: 500">密码要求:8-16包含字母和数字不允许空格</div>
<p v-if="passwordForm.newPassword && !isPasswordValid" class="error-text">
密码格式不符合要求
</p>
</div>
<div class="form-group">
<label for="confirmPassword" class="form-label">确认新密码</label>
@ -92,23 +91,16 @@
<div class="form-actions">
<button type="submit" class="save-button" :disabled="passwordSaving || passwordMismatch">
<button type="submit" class="save-button" :disabled="passwordSaving">
保存新密码
</button>
<button type="submit" class="reset" :disabled="passwordSaving || passwordMismatch">
<button type="button" class="reset" :disabled="passwordSaving" @click="resetPasswordForm" >
重置表单
</button>
</div>
</form>
</div>
</div>
<!-- 操作反馈 -->
<div v-if="successMessage" class="success-message">
{{ successMessage }}
</div>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
</div>
</div>
@ -118,6 +110,7 @@
import { ref, computed, onMounted } from 'vue';
import Menu from '../components/Menu.vue';
import { getUserProfile, updateAvatar, updatePassword } from '../api/profile';
import {ElMessage} from "element-plus";
//
const handleSidebarClick = (menuItem) => {
@ -158,6 +151,11 @@ const passwordMismatch = computed(() => {
const triggerFileInput = () => {
fileInput.value.click();
};
const resetPasswordForm = () => {
passwordForm.value.currentPassword = '';
passwordForm.value.newPassword = '';
passwordForm.value.confirmPassword = '';
};
//
const handleAvatarError = (event) => {
@ -172,13 +170,13 @@ const handleAvatarChange = async (event) => {
if (file) {
//
if (!file.type.startsWith('image/')) {
errorMessage.value = '请选择图片文件';
ElMessage.error("请选择图片文件")
return;
}
// (2MB)
if (file.size > 2 * 1024 * 1024) {
errorMessage.value = '图片大小不能超过2MB';
ElMessage.error("图片大小不能超过2MB")
return;
}
@ -188,23 +186,10 @@ const handleAvatarChange = async (event) => {
errorMessage.value = '';
try {
// localStoragetoken
const token = localStorage.getItem('token');
if (!token) {
errorMessage.value = '用户未登录,请先登录';
avatarUploading.value = false;
return;
}
// FormData
const formData = new FormData();
formData.append('avatar', file);
formData.append('token', token);
console.log('准备上传头像文件:', file.name);
console.log('Token:', token);
// API
const response = await updateAvatar(formData,{
headers: {
@ -226,15 +211,10 @@ const handleAvatarChange = async (event) => {
//
avatarUploadSuccess.value = true;
successMessage.value = '头像更新成功';
ElMessage.success("头像更新成功")
// 3
setTimeout(() => {
avatarUploadSuccess.value = false;
successMessage.value = '';
}, 3000);
} else {
errorMessage.value = response.message || '头像更新失败';
ElMessage.error("头像更新失败")
}
} catch (error) {
console.error('头像上传失败:', error);
@ -247,15 +227,11 @@ const handleAvatarChange = async (event) => {
//
if (error.response) {
//
console.error('服务器响应:', error.response.status, error.response.data);
errorMessage.value = `服务器错误 ${error.response.status}: ${JSON.stringify(error.response.data)}`;
ElMessage.error(`服务器错误 ${error.response.status}: ${JSON.stringify(error.response.data)}`)
} else if (error.request) {
//
console.error('网络错误,未收到响应:', error.request);
errorMessage.value = '网络错误,请检查网络连接和服务器状态';
ElMessage.error("网络错误,请检查网络连接和服务器状态")
} else {
//
errorMessage.value = `请求配置错误: ${error.message}`;
ElMessage.error(`请求配置错误: ${error.message}`)
}
} finally {
avatarUploading.value = false;
@ -263,54 +239,29 @@ const handleAvatarChange = async (event) => {
}
};
//
const updateProfile = () => {
profileSaving.value = true;
errorMessage.value = '';
successMessage.value = '';
// API
setTimeout(() => {
profileSaving.value = false;
successMessage.value = '个人信息更新成功';
// 3
setTimeout(() => {
successMessage.value = '';
}, 3000);
}, 1000);
};
//
const changePassword = async () => {
if (passwordMismatch.value) {
ElMessage.error("两次输入的密码不一致")
return;
}
if (!isPasswordValid.value) {
ElMessage.error("新密码格式不符合要求:8-16位,必须包含字母和数字,且不能有空格")
return;
}
passwordSaving.value = true;
errorMessage.value = '';
successMessage.value = '';
try {
// localStoragetoken
const token = localStorage.getItem('token');
if (!token) {
errorMessage.value = '用户未登录,请先登录';
passwordSaving.value = false;
return;
}
// API
const response = await updatePassword({
token,
currentPassword: passwordForm.value.currentPassword,
newPassword: passwordForm.value.newPassword
});
if (response.success) {
successMessage.value = '密码修改成功';
ElMessage.success("密码修改成功")
//
passwordForm.value = {
currentPassword: '',
@ -318,21 +269,22 @@ const changePassword = async () => {
confirmPassword: ''
};
// 3
setTimeout(() => {
successMessage.value = '';
}, 3000);
} else {
errorMessage.value = response.message || '密码修改失败';
ElMessage.error("密码修改失败")
}
} catch (error) {
console.error('密码修改失败:', error);
errorMessage.value = '密码修改时发生错误';
ElMessage.error("密码修改失败")
} finally {
passwordSaving.value = false;
}
};
const isPasswordValid = computed(() => {
const pwd = passwordForm.value.newPassword;
if (!pwd) return false;
if (pwd.length < 8 || pwd.length > 16) return false;
if (/\s/.test(pwd)) return false;
return /[a-zA-Z]/.test(pwd) && /\d/.test(pwd);
});
//
onMounted(async () => {
try {
@ -362,9 +314,7 @@ onMounted(async () => {
avatar: avatarUrl
};
} else {
console.error('获取用户信息失败:', response.message);
errorMessage.value = response.message || '获取用户信息失败';
ElMessage.error( '获取用户信息失败')
// tokentoken
if (response.message && response.message.includes('登录')) {
localStorage.removeItem('token');
@ -372,12 +322,10 @@ onMounted(async () => {
}
}
} else {
console.log('用户未登录');
errorMessage.value = '用户未登录,请先登录';
ElMessage.error( '用户未登录,请先登录')
}
} catch (error) {
console.error('获取用户信息失败:', error);
errorMessage.value = '获取用户信息时发生错误';
ElMessage.error( '获取用户信息时发生错误')
}
});
</script>
@ -495,6 +443,7 @@ onMounted(async () => {
.avatar-overlay {
position: absolute;
border: 3px solid #e5e7eb;
top: 0;
left: 0;
width: 100%;

Loading…
Cancel
Save