Browse Source

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

# Conflicts:
#	vue/src/components/Menu.vue
#	vue/src/system/KGData.vue
mh
hanyuqing 4 months ago
parent
commit
551375796f
  1. 107
      vue/src/components/Menu.vue
  2. 6
      vue/src/system/GraphDemo.vue
  3. 4
      vue/src/system/GraphQA.vue
  4. 68
      vue/src/system/KGData.vue
  5. 351
      vue/src/system/Profile.vue

107
vue/src/components/Menu.vue

@ -1,13 +1,13 @@
<template> <template>
<div class="admin-layout" :class="{ <div class="admin-layout" :class="{
'is-collapsed': isCollapsed, 'is-collapsed': isCollapsed,
'is-expanded': !isCollapsed 'is-expanded': !isCollapsed
}"> }">
<aside class="sidebar"> <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/logo1.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>
@ -17,73 +17,60 @@
</div> </div>
<nav class="menu-nav"> <nav class="menu-nav">
<div <div v-for="(item, index) in menuItems" :key="index" class="menu-item"
v-for="(item, index) in menuItems" :class="{ 'is-active': activeIndex === index }" @click="handleMenuClick(index)">
:key="index"
class="menu-item"
:class="{ 'is-active': activeIndex === index }"
@click="handleMenuClick(index)"
>
<div class="highlight-box"> <div class="highlight-box">
<div class="menu-content-fixed"> <div class="menu-content-fixed">
<div class="menu-icon-wrapper"> <div class="menu-icon-wrapper">
<img :src="item.icon" class="menu-icon-img"/> <img :src="item.icon" class="menu-icon-img" />
</div> </div>
<span v-if="!isCollapsed" class="menu-text">{{ item.name }}</span> <span v-if="!isCollapsed" class="menu-text">{{ item.name }}</span>
</div> </div>
<img v-if="activeIndex === index && !isCollapsed" src="@/assets/右侧白色.png" class="active-tag"/> <img v-if="activeIndex === index && !isCollapsed" src="@/assets/右侧白色.png" class="active-tag" />
</div> </div>
</div> </div>
<div v-if="!isCollapsed" class="collapse-handle" @click="isCollapsed = true"> <div v-if="!isCollapsed" class="collapse-handle" @click="isCollapsed = true">
<img src="@/assets/收缩.png" class="collapse-icon-img"/> <img src="@/assets/收缩.png" class="collapse-icon-img" />
</div> </div>
</nav> </nav>
<div class="sidebar-footer" <div class="sidebar-footer" :style="{
:style="{ 'border-top': isCollapsed ? 'none' : '2px solid rgba(255, 255, 255, 0.1)'
'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">
<img <img :src="userProfile.avatar" alt="用户头像" class="avatar" @click="handleProfile">
:src="userProfile.avatar"
alt="用户头像"
class="avatar"
@click="handleProfile"
>
<div class="info"> <div class="info">
<div class="name" @click="handleProfile">{{ userProfile.username }}</div> <div class="name" @click="handleProfile">{{ userProfile.username }}</div>
<div class="id">8866990099</div> <div class="id">8866990099</div>
</div> </div>
<div class="exit-wrap"> <div class="exit-wrap">
<img src="@/assets/退出.png" class="exit-icon" @click="handleLogout" alt="退出"/> <img src="@/assets/退出.png" class="exit-icon" @click="handleLogout" alt="退出" />
</div> </div>
</div> </div>
<div v-else class="expand-handle-circle" @click="isCollapsed = false"> <div v-else class="expand-handle-circle" @click="isCollapsed = false">
<img src="@/assets/打开.png" class="expand-icon-img-circle"/> <img src="@/assets/打开.png" class="expand-icon-img-circle" />
</div> </div>
</div> </div>
</aside> </aside>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from 'vue'; import { onMounted, ref } from 'vue';
import {useRouter} from 'vue-router'; import { useRouter } from 'vue-router';
//
import { ElMessage } from 'element-plus';
import i1 from '@/assets/图标1.png'; import i1 from '@/assets/图标1.png';
import i2 from '@/assets/图标2.png'; import i2 from '@/assets/图标2.png';
import i3 from '@/assets/图标3.png'; import i3 from '@/assets/图标3.png';
import i4 from '@/assets/图标4.png'; import i4 from '@/assets/图标4.png';
import {getUserProfile} from "@/api/profile"; import { getUserProfile } from "@/api/profile";
const router = useRouter(); const router = useRouter();
// const activeIndex = ref(0);
// //
const props = defineProps({ const props = defineProps({
//
initialActive: { initialActive: {
type: Number, type: Number,
default: 0 default: 0
@ -92,10 +79,12 @@ const props = defineProps({
// //
const emit = defineEmits(['menu-click']); const emit = defineEmits(['menu-click']);
const userProfile = ref({ const userProfile = ref({
username: '用户', username: '用户',
avatar: '/resource/avatar/用户.png' avatar: '/resource/avatar/用户.png'
}); });
// //
const activeIndex = ref(props.initialActive); const activeIndex = ref(props.initialActive);
@ -103,65 +92,65 @@ const activeIndex = ref(props.initialActive);
const isCollapsed = ref(false); const isCollapsed = ref(false);
const menuItems = [ const menuItems = [
{name: '医疗知识图谱', path: '/kg-display', icon: i1}, { name: '医疗知识图谱', path: '/kg-display', icon: i1 },
{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-style', 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 handleProfile = () => { const handleProfile = () => {
// 使Vue Router
router.push('/profile'); router.push('/profile');
}; };
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('messages'); localStorage.removeItem('messages');
// 使Vue Router localStorage.removeItem('token'); // 退token
router.push('/login'); router.push('/login');
}; };
onMounted(async () => { onMounted(async () => {
try { try {
// localStoragetoken
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
console.log('Profile组件挂载,获取到的token:', token); console.log('Profile组件挂载,获取到的token:', token);
if (token) { if (token) {
// API
const response = await getUserProfile(token); const response = await getUserProfile(token);
if (response.success) { if (response.success) {
// let avatarUrl = response.user.avatar || '/resource/avatar/4.png';
// //
let avatarUrl = response.user.avatar || '/resource/avatar/用户.png';
if (avatarUrl.startsWith('/resource/')) {
avatarUrl = avatarUrl; // 使
}
console.log('设置头像URL:', avatarUrl);
userProfile.value = { userProfile.value = {
username: response.user.username, username: response.user.username,
avatar: avatarUrl avatar: avatarUrl
}; };
} else { } else {
// tokentoken // token
if (response.message && response.message.includes('登录')) { if (response.message && response.message.includes('登录')) {
localStorage.removeItem('token'); localStorage.removeItem('token');
ElMessage.warning('登录已过期,请重新登录');
} }
} }
} else { } else {
console.log('用户未登录'); console.log('用户未登录');
errorMessage.value = '用户未登录,请先登录'; // ElMessage errorMessage.value
ElMessage.info('您当前处于游客模式,请登录后操作');
} }
} catch (error) { } catch (error) {
console.error('获取用户信息失败:', error); console.error('获取用户信息失败:', error);
errorMessage.value = '获取用户信息时发生错误'; // ElMessage
ElMessage.error('无法连接到服务器,请检查网络');
} }
}); });
</script> </script>
<style scoped> <style scoped>
/* 样式部分保持不变,由于你提供的是完整样式,此处完全保留 */
.admin-layout { .admin-layout {
display: flex; display: flex;
height: 100vh; height: 100vh;
@ -175,7 +164,6 @@ onMounted(async () => {
flex-shrink: 0; flex-grow: 0; flex-shrink: 0; flex-grow: 0;
} }
/* --- 侧边栏 --- */
.sidebar { .sidebar {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -195,10 +183,9 @@ onMounted(async () => {
width: 12%; width: 12%;
} }
.sidebar-header { .sidebar-header {
padding: 25px 0px 18px 0; padding: 25px 0px 18px 20px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1); border-bottom: 2px solid rgba(255, 255, 255, 0.1);
display: flex; justify-content: flex-start;
justify-content: center;
} }
.header-content { .header-content {
@ -239,7 +226,6 @@ onMounted(async () => {
font-weight: 600; font-weight: 600;
} }
/* --- 菜单导航 --- */
.menu-nav { .menu-nav {
flex: 1; flex: 1;
padding-top: 10px; padding-top: 10px;
@ -333,7 +319,6 @@ onMounted(async () => {
width: 14px; width: 14px;
} }
/* --- 用户区域 (重点修改) --- */
.sidebar-footer { .sidebar-footer {
padding: 12px 0 20px 0; padding: 12px 0 20px 0;
border-top: 2px solid rgba(255, 255, 255, 0.1); border-top: 2px solid rgba(255, 255, 255, 0.1);
@ -378,7 +363,7 @@ onMounted(async () => {
font-size: 9px; font-size: 9px;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
line-height: 1; line-height: 1;
margin-top: 6px; /* 通过增加上边距让id向下移动 */ margin-top: 6px;
} }
.exit-wrap { .exit-wrap {
@ -403,6 +388,4 @@ onMounted(async () => {
width: 32px; width: 32px;
height: 32px; height: 32px;
} }
</style> </style>

6
vue/src/system/GraphDemo.vue

@ -597,13 +597,13 @@ export default {
labelText: (d) => d.data.relationship.properties.label, labelText: (d) => d.data.relationship.properties.label,
stroke: (d) => { stroke: (d) => {
// target label // target label
const targetLabel = this._nodeLabelMap.get(d.target); // d.target ID const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
// target // target
if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)'; if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)'; if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)'; if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
if (targetLabel === 'Check') return 'rgba(89,209,212,0.5)'; // if (targetLabel === 'Check') return 'rgba(51,110,238,0.5)'; //
return 'rgba(51,110,238,0.5)'; // default return 'rgba(89,209,212,0.5)'; // default
}, },
// labelFill: (d) => { // labelFill: (d) => {
// // target label // // target label

4
vue/src/system/GraphQA.vue

@ -60,7 +60,7 @@ export default {
data() { data() {
return { return {
query:"", query:"",
answers:[] answers:['糖尿病不能吃什么','糖尿病不能吃什么','糖尿病不能吃什么','糖尿病不能吃什么','糖尿病不能吃什么',]
}; };
}, },
methods: { methods: {
@ -68,7 +68,7 @@ export default {
let data={ let data={
text:this.query text:this.query
} }
qaAnalyze(data).then(res=>{ qaAnalyze().then(res=>{
}) })
} }

68
vue/src/system/KGData.vue

@ -27,7 +27,7 @@
<div class="stat-inner"> <div class="stat-inner">
<div class="stat-label">今日新增</div> <div class="stat-label">今日新增</div>
<div class="stat-value">{{ stats.todayNodes.toLocaleString() }}</div> <div class="stat-value">{{ stats.todayNodes.toLocaleString() }}</div>
<div class="stat-desc">24小时内系统自动爬取及人工审核增量</div> <div class="stat-desc">24小时内系统人工审核增量</div>
</div> </div>
</div> </div>
</div> </div>
@ -83,7 +83,7 @@
</div> </div>
<div class="table-shadow-wrapper table-compact"> <div class="table-shadow-wrapper table-compact">
<el-table v-loading="loading" :data="nodeData" class="ref-table" height="calc(100vh - 560px)"> <el-table v-loading="loading" :data="nodeData" class="ref-table" height="calc(100vh - 530px)">
<el-table-column prop="nodeId" label="节点ID" width="180" align="center" show-overflow-tooltip/> <el-table-column prop="nodeId" label="节点ID" width="180" align="center" show-overflow-tooltip/>
<el-table-column label="实体类型" width="160" align="center"> <el-table-column label="实体类型" width="160" align="center">
<template #default="scope"> <template #default="scope">
@ -208,8 +208,7 @@
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
<el-dialog v-model="nodeDialogVisible" :title="isEdit ? '修改节点' : '新增节点'" width="450px" class="custom-dialog" <el-dialog v-model="nodeDialogVisible" :title="isEdit ? '修改节点' : '新增节点'" width="450px" class="custom-dialog" header-class="bold-header">
header-class="bold-header">
<el-form :model="nodeForm" label-width="90px" class="custom-form"> <el-form :model="nodeForm" label-width="90px" class="custom-form">
<el-form-item label="名称" required> <el-form-item label="名称" required>
<el-input v-model="nodeForm.name" placeholder="请输入实体名称" clearable/> <el-input v-model="nodeForm.name" placeholder="请输入实体名称" clearable/>
@ -228,16 +227,13 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="relDialogVisible" :title="isEdit ? '修改关系' : '新增关系'" width="450px" class="custom-dialog" <el-dialog v-model="relDialogVisible" :title="isEdit ? '修改关系' : '新增关系'" width="450px" class="custom-dialog" header-class="bold-header">
header-class="bold-header">
<el-form :model="relForm" label-width="90px" class="custom-form"> <el-form :model="relForm" label-width="90px" class="custom-form">
<el-form-item label="起始节点" required> <el-form-item label="起始节点" required>
<el-autocomplete v-model="relForm.source" :fetch-suggestions="queryNodeSearch" style="width:100%" <el-autocomplete v-model="relForm.source" :fetch-suggestions="queryNodeSearch" style="width:100%" placeholder="请输入起点名称"/>
placeholder="请输入起点名称"/>
</el-form-item> </el-form-item>
<el-form-item label="结束节点" required> <el-form-item label="结束节点" required>
<el-autocomplete v-model="relForm.target" :fetch-suggestions="queryNodeSearch" style="width:100%" <el-autocomplete v-model="relForm.target" :fetch-suggestions="queryNodeSearch" style="width:100%" placeholder="请输入终点名称"/>
placeholder="请输入终点名称"/>
</el-form-item> </el-form-item>
<el-form-item label="关系类型" required> <el-form-item label="关系类型" required>
<el-input v-model="relForm.type" placeholder="例如:TREATS"/> <el-input v-model="relForm.type" placeholder="例如:TREATS"/>
@ -249,9 +245,7 @@
<template #footer> <template #footer>
<div class="dialog-footer-wrap"> <div class="dialog-footer-wrap">
<el-button class="btn-cancel" @click="relDialogVisible = false">取消</el-button> <el-button class="btn-cancel" @click="relDialogVisible = false">取消</el-button>
<el-button class="btn-confirm" type="primary" :loading="submitting" :disabled="submitting" @click="submitRel"> <el-button class="btn-confirm" type="primary" :loading="submitting" :disabled="submitting" @click="submitRel">确认</el-button>
确认
</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -259,8 +253,8 @@
</template> </template>
<script setup> <script setup>
import {ref, onMounted, reactive} from 'vue' import { ref, onMounted, reactive } from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import Menu from '@/components/Menu.vue' import Menu from '@/components/Menu.vue'
import { import {
getKgStats, getLabels, getNodeSuggestions, getNodesList, getRelationshipsList, getKgStats, getLabels, getNodeSuggestions, getNodesList, getRelationshipsList,
@ -272,18 +266,18 @@ const pageSize = ref(20);
const activeName = ref('first'); const activeName = ref('first');
const loading = ref(false); const loading = ref(false);
const submitting = ref(false); // const submitting = ref(false); //
const stats = reactive({totalNodes: 0, totalRels: 0, todayNodes: 0}); const stats = reactive({ totalNodes: 0, totalRels: 0, todayNodes: 0 });
// --- --- // --- ---
const nodeData = ref([]); const nodeData = ref([]);
const nodeTotal = ref(0); const nodeTotal = ref(0);
const nodePage = ref(1); const nodePage = ref(1);
const nodeSearch = reactive({name: '', label: ''}); const nodeSearch = reactive({ name: '', label: '' });
const relData = ref([]); const relData = ref([]);
const relTotal = ref(0); const relTotal = ref(0);
const relPage = ref(1); const relPage = ref(1);
const relSearch = reactive({source: '', target: ''}); const relSearch = reactive({ source: '', target: '' });
// --- --- // --- ---
const nodeDialogVisible = ref(false); const nodeDialogVisible = ref(false);
@ -295,8 +289,8 @@ const isEdit = ref(false);
const dynamicLabels = ref([]); const dynamicLabels = ref([]);
// --- --- // --- ---
const nodeForm = reactive({id: '', name: '', label: ''}); const nodeForm = reactive({ id: '', name: '', label: '' });
const relForm = reactive({id: '', source: '', target: '', type: '', label: ''}); const relForm = reactive({ id: '', source: '', target: '', type: '', label: '' });
// --- --- // --- ---
@ -304,9 +298,7 @@ const fetchStats = async () => {
try { try {
const res = await getKgStats(); const res = await getKgStats();
if (res?.code === 200) Object.assign(stats, res.data); if (res?.code === 200) Object.assign(stats, res.data);
} catch (e) { } catch (e) { console.error("Stats Error", e); }
console.error("Stats Error", e);
}
}; };
const fetchNodes = async () => { const fetchNodes = async () => {
@ -322,11 +314,8 @@ const fetchNodes = async () => {
nodeData.value = res.data.items; nodeData.value = res.data.items;
nodeTotal.value = res.data.total; nodeTotal.value = res.data.total;
} }
} catch (e) { } catch (e) { ElMessage.error('加载节点失败'); }
ElMessage.error('加载节点失败'); finally { loading.value = false; }
} finally {
loading.value = false;
}
}; };
const fetchRels = async () => { const fetchRels = async () => {
@ -342,19 +331,16 @@ const fetchRels = async () => {
relData.value = res.data.items; relData.value = res.data.items;
relTotal.value = res.data.total; relTotal.value = res.data.total;
} }
} catch (e) { } catch (e) { ElMessage.error('加载关系失败'); }
ElMessage.error('加载关系失败'); finally { loading.value = false; }
} finally {
loading.value = false;
}
}; };
const openNodeDialog = (row = null) => { const openNodeDialog = (row = null) => {
isEdit.value = !!row; isEdit.value = !!row;
if (row) { if (row) {
Object.assign(nodeForm, {id: row.id, name: row.name, label: row.labels?.[0] || ''}); Object.assign(nodeForm, { id: row.id, name: row.name, label: row.labels?.[0] || '' });
} else { } else {
Object.assign(nodeForm, {id: '', name: '', label: 'Drug'}); Object.assign(nodeForm, { id: '', name: '', label: 'Drug' });
} }
nodeDialogVisible.value = true; nodeDialogVisible.value = true;
}; };
@ -376,21 +362,13 @@ const submitNode = async () => {
} }
} catch (e) { } catch (e) {
ElMessage.error('接口响应异常'); ElMessage.error('接口响应异常');
} finally { } finally { submitting.value = false; }
submitting.value = false;
}
}; };
const openRelDialog = (row = null) => { const openRelDialog = (row = null) => {
isEdit.value = !!row; isEdit.value = !!row;
if (row) { if (row) {
Object.assign(relForm, { Object.assign(relForm, { id: row.id, source: row.source, target: row.target, type: row.type, label: row.label || '' });
id: row.id,
source: row.source,
target: row.target,
type: row.type,
label: row.label || ''
});
} else { } else {
Object.assign(relForm, {id: '', source: '', target: '', type: '', label: ''}); Object.assign(relForm, {id: '', source: '', target: '', type: '', label: ''});
} }

351
vue/src/system/Profile.vue

@ -4,92 +4,99 @@
<Menu <Menu
@menu-click="handleSidebarClick" @menu-click="handleSidebarClick"
/> />
<!-- 主内容区域 --> <!-- 主内容区域 -->
<div class="main-content"> <div class="main-content">
<div class="profile-container"> <div class="profile-container">
<!-- 页面标题 --> <!-- 页面标题 -->
<div class="page-header"> <div class="page-header">
<h1 class="page-title">个人主页</h1> <h2 class="page-title">个人中心</h2>
<p class="page-subtitle">管理您的个人信息和账户设置</p>
</div> </div>
<div class="avatar-section">
<!-- 个人信息卡片 --> <div class="avatar-container">
<div class="profile-card"> <img
<!-- 头像区域 --> :src="userProfile.avatar"
<div class="avatar-section"> alt="用户头像"
<div class="avatar-container">
<img
:src="userProfile.avatar"
alt="用户头像"
class="avatar-image" class="avatar-image"
@error="handleAvatarError" @error="handleAvatarError"
> >
<div class="avatar-overlay" @click="triggerFileInput"> <div class="avatar-overlay" @click="triggerFileInput">
<span class="camera-icon">📷</span> <span class="camera-icon">📷</span>
<span class="overlay-text">更换头像</span> <span class="overlay-text">更换头像</span>
</div>
<input
type="file"
ref="fileInput"
@change="handleAvatarChange"
accept="image/*"
class="hidden-input"
>
</div> </div>
<div class="username-display">{{ userProfile.username }}</div> <input
<div class="avatar-status"> type="file"
<p v-if="avatarUploading" class="uploading-text">上传中...</p> ref="fileInput"
<p v-if="avatarUploadSuccess" class="success-text">头像更新成功</p> @change="handleAvatarChange"
accept="image/*"
class="hidden-input"
>
</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>
</div>
<!-- 个人信息卡片 -->
<div class="profile-card">
<!-- 头像区域 -->
<!-- 用户信息表单 --> <!-- 用户信息表单 -->
<div class="info-section"> <div class="info-section">
<h2 class="section-title">修改密码</h2> <div style="border-bottom: 3px solid #F5F8FF;">
<h2 class="section-title">修改密码</h2>
</div>
<form class="password-form" @submit.prevent="changePassword"> <form class="password-form" @submit.prevent="changePassword">
<div class="form-group"> <div class="form-group">
<label for="currentPassword" class="form-label">当前密码</label> <label for="currentPassword" class="form-label">当前密码</label>
<input <input
type="password" type="password"
id="currentPassword" id="currentPassword"
v-model="passwordForm.currentPassword" v-model="passwordForm.currentPassword"
class="form-input" class="form-input"
placeholder="请输入当前密码" placeholder="请输入当前登录密码"
required required
> >
</div> </div>
<div style="display: flex">
<div class="form-group"> <div class="form-group">
<label for="newPassword" class="form-label">新密码</label> <label for="newPassword" class="form-label">新密码</label>
<input <input
type="password" type="password"
id="newPassword" id="newPassword"
v-model="passwordForm.newPassword" v-model="passwordForm.newPassword"
class="form-input" class="form-input"
placeholder="请输入新密码" placeholder="请设置新密码(8-16位,含字母+数字)"
required required style=" width: 97%;"
> >
</div> <div class="form-label" style="text-align: left;margin-top: 5px;font-size: 13px;font-weight: 500">密码要求:8-16包含字母和数字不允许空格</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="confirmPassword" class="form-label">确认新密码</label> <label for="confirmPassword" class="form-label">确认新密码</label>
<input <input style=" width: 100%;"
type="password" type="password"
id="confirmPassword" id="confirmPassword"
v-model="passwordForm.confirmPassword" v-model="passwordForm.confirmPassword"
class="form-input" class="form-input"
placeholder="请再次输入新密码" placeholder="请再次输入新密码"
required required
> >
<p v-if="passwordMismatch" class="error-text">两次输入的密码不一致</p> <p v-if="passwordMismatch" class="error-text">两次输入的密码不一致</p>
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="save-button" :disabled="passwordSaving || passwordMismatch"> <button type="submit" class="save-button" :disabled="passwordSaving || passwordMismatch">
<span v-if="!passwordSaving">修改密码</span> 保存新密码
<span v-else>修改中...</span> </button>
<button type="submit" class="reset" :disabled="passwordSaving || passwordMismatch">
重置表单
</button> </button>
</div> </div>
</form> </form>
@ -141,8 +148,8 @@ const errorMessage = ref('');
// //
const passwordMismatch = computed(() => { const passwordMismatch = computed(() => {
return passwordForm.value.newPassword && return passwordForm.value.newPassword &&
passwordForm.value.confirmPassword && passwordForm.value.confirmPassword &&
passwordForm.value.newPassword !== passwordForm.value.confirmPassword; passwordForm.value.newPassword !== passwordForm.value.confirmPassword;
}); });
@ -168,45 +175,45 @@ const handleAvatarChange = async (event) => {
errorMessage.value = '请选择图片文件'; errorMessage.value = '请选择图片文件';
return; return;
} }
// (2MB) // (2MB)
if (file.size > 2 * 1024 * 1024) { if (file.size > 2 * 1024 * 1024) {
errorMessage.value = '图片大小不能超过2MB'; errorMessage.value = '图片大小不能超过2MB';
return; return;
} }
// //
avatarUploading.value = true; avatarUploading.value = true;
avatarUploadSuccess.value = false; avatarUploadSuccess.value = false;
errorMessage.value = ''; errorMessage.value = '';
try { try {
// localStoragetoken // localStoragetoken
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
errorMessage.value = '用户未登录,请先登录'; errorMessage.value = '用户未登录,请先登录';
avatarUploading.value = false; avatarUploading.value = false;
return; return;
} }
// FormData // FormData
const formData = new FormData(); const formData = new FormData();
formData.append('avatar', file); formData.append('avatar', file);
formData.append('token', token); formData.append('token', token);
console.log('准备上传头像文件:', file.name); console.log('准备上传头像文件:', file.name);
console.log('Token:', token); console.log('Token:', token);
// API // API
const response = await updateAvatar(formData,{ const response = await updateAvatar(formData,{
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} }
}); });
console.log('收到响应:', response); console.log('收到响应:', response);
if (response.success) { if (response.success) {
// 使 // 使
// URL // URL
@ -216,11 +223,11 @@ const handleAvatarChange = async (event) => {
avatarPath = avatarPath; avatarPath = avatarPath;
} }
userProfile.value.avatar = avatarPath; userProfile.value.avatar = avatarPath;
// //
avatarUploadSuccess.value = true; avatarUploadSuccess.value = true;
successMessage.value = '头像更新成功'; successMessage.value = '头像更新成功';
// 3 // 3
setTimeout(() => { setTimeout(() => {
avatarUploadSuccess.value = false; avatarUploadSuccess.value = false;
@ -236,7 +243,7 @@ const handleAvatarChange = async (event) => {
stack: error.stack, stack: error.stack,
name: error.name name: error.name
}); });
// //
if (error.response) { if (error.response) {
// //
@ -261,12 +268,12 @@ const updateProfile = () => {
profileSaving.value = true; profileSaving.value = true;
errorMessage.value = ''; errorMessage.value = '';
successMessage.value = ''; successMessage.value = '';
// API // API
setTimeout(() => { setTimeout(() => {
profileSaving.value = false; profileSaving.value = false;
successMessage.value = '个人信息更新成功'; successMessage.value = '个人信息更新成功';
// 3 // 3
setTimeout(() => { setTimeout(() => {
successMessage.value = ''; successMessage.value = '';
@ -279,38 +286,38 @@ const changePassword = async () => {
if (passwordMismatch.value) { if (passwordMismatch.value) {
return; return;
} }
passwordSaving.value = true; passwordSaving.value = true;
errorMessage.value = ''; errorMessage.value = '';
successMessage.value = ''; successMessage.value = '';
try { try {
// localStoragetoken // localStoragetoken
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
errorMessage.value = '用户未登录,请先登录'; errorMessage.value = '用户未登录,请先登录';
passwordSaving.value = false; passwordSaving.value = false;
return; return;
} }
// API // API
const response = await updatePassword({ const response = await updatePassword({
token, token,
currentPassword: passwordForm.value.currentPassword, currentPassword: passwordForm.value.currentPassword,
newPassword: passwordForm.value.newPassword newPassword: passwordForm.value.newPassword
}); });
if (response.success) { if (response.success) {
successMessage.value = '密码修改成功'; successMessage.value = '密码修改成功';
// //
passwordForm.value = { passwordForm.value = {
currentPassword: '', currentPassword: '',
newPassword: '', newPassword: '',
confirmPassword: '' confirmPassword: ''
}; };
// 3 // 3
setTimeout(() => { setTimeout(() => {
successMessage.value = ''; successMessage.value = '';
@ -331,15 +338,15 @@ onMounted(async () => {
try { try {
// localStoragetoken // localStoragetoken
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
console.log('Profile组件挂载,获取到的token:', token); console.log('Profile组件挂载,获取到的token:', token);
if (token) { if (token) {
// API // API
const response = await getUserProfile(token); const response = await getUserProfile(token);
console.log('获取用户信息响应:', response); console.log('获取用户信息响应:', response);
if (response.success) { if (response.success) {
// //
// //
@ -347,9 +354,9 @@ onMounted(async () => {
if (avatarUrl.startsWith('/resource/')) { if (avatarUrl.startsWith('/resource/')) {
avatarUrl = avatarUrl; // 使 avatarUrl = avatarUrl; // 使
} }
console.log('设置头像URL:', avatarUrl); console.log('设置头像URL:', avatarUrl);
userProfile.value = { userProfile.value = {
username: response.user.username, username: response.user.username,
avatar: avatarUrl avatar: avatarUrl
@ -357,7 +364,7 @@ onMounted(async () => {
} else { } else {
console.error('获取用户信息失败:', response.message); console.error('获取用户信息失败:', response.message);
errorMessage.value = response.message || '获取用户信息失败'; errorMessage.value = response.message || '获取用户信息失败';
// tokentoken // tokentoken
if (response.message && response.message.includes('登录')) { if (response.message && response.message.includes('登录')) {
localStorage.removeItem('token'); localStorage.removeItem('token');
@ -376,40 +383,23 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
.app-container {
.app-container{
display: flex; display: flex;
height: 100vh; height: 100vh;
overflow: hidden;
} }
.main-content { .main-content {
display: flex;
height: 100vh;
background-color: #F5F8FF;
flex: 1; flex: 1;
overflow: auto;;
padding: 1.5rem; padding: 1.5rem;
overflow-y: auto;
height: 100%;
background-color: #f5f7fa;
transition: margin-left 0.3s ease; transition: margin-left 0.3s ease;
} }
/* 确保左侧导航栏完全固定 */
.app-container > :first-child {
position: fixed;
height: 100vh;
z-index: 10;
}
/* 为右侧内容添加左边距,避免被固定导航栏遮挡 */
.main-content {
margin-left: 240px; /* 与Menu.vue中的sidebar-container宽度相同 */
}
/* 当菜单折叠时调整内容区域 */
.app-container:has(.sidebar-container.collapsed) .main-content {
margin-left: 60px;
}
.profile-container { .profile-container {
max-width: 700px; width: 97%;
margin: 0 auto; margin: 0 auto;
} }
@ -418,13 +408,30 @@ onMounted(async () => {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.page-title { .page-title {
font-size: 1.5rem; font-size: 20px;
font-weight: 600; margin-bottom: 20px;
color: #1f2937; padding-left: 12px;
margin: 0 0 0.5rem 0; position: relative;
display: flex;
align-items: center;
color: #165DFF;
margin-left: 15px;
} }
.page-title::before {
content: '';
width: 6px;
height: 18px;
background-color: #165DFF;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
border-radius: 5px;
}
.page-subtitle { .page-subtitle {
color: #6b7280; color: #6b7280;
margin: 0; margin: 0;
@ -450,13 +457,12 @@ onMounted(async () => {
/* 个人信息卡片 */ /* 个人信息卡片 */
.profile-card { .profile-card {
background-color: #fff; background-color: #fff;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
max-width: 600px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
box-shadow: 2px -1px 14px 9px #EBF1FF;
border-radius: 18px;
} }
/* 头像区域样式 */ /* 头像区域样式 */
@ -465,14 +471,17 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 2rem; margin-bottom: 2rem;
padding-bottom: 1.5rem; padding: 20px;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
background-color: #fff;
box-shadow: 2px -1px 14px 9px #EBF1FF;
border-radius: 18px;
} }
.avatar-container { .avatar-container {
position: relative; position: relative;
width: 100px; width: 140px;
height: 100px; height: 140px;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -525,11 +534,14 @@ onMounted(async () => {
.username-display { .username-display {
text-align: center; text-align: center;
font-size: 1.125rem; font-size: 12px;
font-weight: 600; color: #1356F2;
color: #1f2937;
margin-top: 0.75rem; margin-top: 0.75rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
background-color: #E8F0FF;
padding: 10px 13px;
border-radius: 5px;
font-weight:600;
} }
.uploading-text, .success-text { .uploading-text, .success-text {
@ -545,28 +557,34 @@ onMounted(async () => {
color: #10b981; color: #10b981;
} }
/* 信息区域样式 */
.info-section {
margin-bottom: 1.5rem;
}
.section-title { .section-title {
font-size: 1.125rem; font-size: 17px;
font-weight: 600; margin-bottom: 10px;
color: #1f2937; padding-left: 12px;
margin: 0 0 1rem 0; position: relative;
padding-bottom: 0.5rem; display: flex;
border-bottom: 1px solid #e5e7eb; align-items: center;
text-align: center; color: #165DFF;
margin-top: 0px;
} }
.section-title::before {
content: '';
width: 6px;
height: 17px;
background-color: #165DFF;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
border-radius: 5px;
}
/* 表单样式 */ /* 表单样式 */
.profile-form, .password-form { .profile-form, .password-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 13px;
max-width: 400px; margin-top: 15px;
margin: 0 auto;
} }
.form-group { .form-group {
@ -576,20 +594,22 @@ onMounted(async () => {
} }
.form-label { .form-label {
font-size: 0.875rem; font-size: 15px;
font-weight: 500; text-align: left;
color: #374151; font-weight: bold;
color: #8d929a;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.form-input { .form-input {
padding: 0.625rem 0.875rem; padding: 0.625rem 0.875rem;
border: 1px solid #d1d5db; border: 2px solid #EBF1FF;
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 0.875rem; font-size: 0.875rem;
transition: border-color 0.2s; transition: border-color 0.2s;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
background-color: #F5F8FF;
} }
.form-input:focus { .form-input:focus {
@ -608,38 +628,43 @@ onMounted(async () => {
} }
.form-actions { .form-actions {
display: flex; text-align: left;
justify-content: center; margin-top: -2px;
margin-top: 1rem;
} }
.save-button { .save-button {
background-color: #3b82f6; background-color: #1558F6;
color: white; color: white;
border: none; border: none;
border-radius: 0.375rem;
padding: 0.625rem 1.25rem; padding: 0.625rem 1.25rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
min-width: 120px; min-width: 120px;
border-radius: 7px;
margin-right: 15px;
} }
.save-button:hover:not(:disabled) { .reset {
background-color: #2563eb; background-color: #F5F8FF;
} color: #8d929a;
padding: 0.625rem 1.25rem;
.save-button:disabled { font-size: 0.875rem;
background-color: #9ca3af; font-weight: 500;
cursor: not-allowed; cursor: pointer;
transition: background-color 0.2s;
min-width: 120px;
border-radius: 7px;
border: 2px solid #EBF1FF;
} }
/* 错误和成功消息样式 */ /* 错误和成功消息样式 */
.error-text { .error-text {
color: #ef4444; color: #ef4444;
font-size: 0.75rem; text-align: left;
margin-top: 0.25rem; margin-top: 5px;
font-size: 13px;
font-weight: 500;
} }
.success-message, .error-message { .success-message, .error-message {

Loading…
Cancel
Save