添加个人中心页面:创建Profile.vue,添加路由,修复Navbar个人信息菜单点击事件

This commit is contained in:
Your Name 2026-01-02 14:43:50 +08:00
parent a6ebea2438
commit 8b599d2226
3 changed files with 540 additions and 4 deletions

View File

@ -34,17 +34,17 @@
</div>
</div>
<template #overlay>
<a-menu>
<a-menu @click="handleMenuClick">
<a-menu-item key="profile">
<Icon name="user" size="sm" style="margin-right: 8px;" />
个人信息
个人中心
</a-menu-item>
<a-menu-item v-if="isAdmin" key="settings" @click="handleSettings">
<a-menu-item v-if="isAdmin" key="settings">
<Icon name="settings" size="sm" style="margin-right: 8px;" />
系统设置
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="handleLogout">
<a-menu-item key="logout">
<Icon name="logout" size="sm" style="margin-right: 8px;" />
退出登录
</a-menu-item>
@ -149,6 +149,17 @@ const handleAchievements = () => {
router.push('/achievements')
}
// 处理菜单点击
const handleMenuClick = ({ key }: { key: string }) => {
if (key === 'profile') {
router.push('/profile')
} else if (key === 'settings') {
router.push('/settings')
} else if (key === 'logout') {
handleLogout()
}
}
// 处理设置点击
const handleSettings = () => {
router.push('/settings')

View File

@ -120,6 +120,14 @@ const routes: RouteRecordRaw[] = [
title: '我的目标'
}
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/Profile.vue'),
meta: {
title: '个人中心'
}
},
{
path: '/team-pk',
name: 'TeamPK',

View File

@ -0,0 +1,517 @@
<template>
<DefaultLayout>
<div class="profile-page">
<div class="container">
<div class="profile-header">
<h1 class="page-title">个人中心</h1>
</div>
<div class="profile-content">
<a-card class="profile-card">
<a-tabs v-model:activeKey="activeTab" type="card">
<!-- 基本信息 -->
<a-tab-pane key="basic" tab="基本信息">
<a-form
ref="formRef"
:model="userForm"
:rules="formRules"
layout="vertical"
class="profile-form"
>
<!-- 头像上传 -->
<a-form-item label="头像">
<div class="avatar-upload-section">
<div class="avatar-preview">
<img
v-if="userForm.avatar"
:src="userForm.avatar"
alt="头像"
class="avatar-preview-image"
/>
<div v-else class="avatar-placeholder">
<Icon name="user" size="xl" color="#6B6B6B" />
</div>
</div>
<div class="avatar-upload-actions">
<a-upload
:before-upload="handleAvatarUpload"
:show-upload-list="false"
accept="image/*"
>
<a-button type="primary">
<template #icon><Icon name="upload" size="sm" /></template>
上传头像
</a-button>
</a-upload>
<a-button
v-if="userForm.avatar"
danger
@click="handleRemoveAvatar"
style="margin-left: 8px;"
>
<template #icon><Icon name="trash" size="sm" /></template>
删除头像
</a-button>
</div>
<div class="upload-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
建议尺寸:200x200px,支持 PNG、JPG 格式
</div>
</div>
</a-form-item>
<!-- 用户名(只读) -->
<a-form-item label="用户名">
<a-input
v-model:value="userForm.username"
disabled
size="large"
/>
</a-form-item>
<!-- 真实姓名 -->
<a-form-item label="真实姓名" name="realName">
<a-input
v-model:value="userForm.realName"
placeholder="请输入真实姓名"
size="large"
/>
</a-form-item>
<!-- 邮箱 -->
<a-form-item label="邮箱" name="email">
<a-input
v-model:value="userForm.email"
placeholder="请输入邮箱"
size="large"
/>
</a-form-item>
<!-- 手机号 -->
<a-form-item label="手机号" name="phone">
<a-input
v-model:value="userForm.phone"
placeholder="请输入手机号"
size="large"
/>
</a-form-item>
<!-- 部门(只读) -->
<a-form-item label="所属部门">
<a-input
v-if="userInfo.deptName"
:value="userInfo.deptName"
disabled
size="large"
/>
<a-input
v-else
value="未分配部门"
disabled
size="large"
/>
</a-form-item>
<!-- 角色(只读) -->
<a-form-item label="角色">
<a-tag
v-for="role in userInfo.roles"
:key="role"
color="blue"
style="margin-right: 8px;"
>
{{ role }}
</a-tag>
</a-form-item>
<!-- 保存按钮 -->
<a-form-item>
<a-button
type="primary"
size="large"
@click="handleSaveProfile"
:loading="saving"
>
<template #icon><Icon name="check" size="sm" /></template>
保存修改
</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<!-- 修改密码 -->
<a-tab-pane key="password" tab="修改密码">
<a-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
layout="vertical"
class="profile-form"
>
<a-form-item label="当前密码" name="oldPassword">
<a-input-password
v-model:value="passwordForm.oldPassword"
placeholder="请输入当前密码"
size="large"
/>
</a-form-item>
<a-form-item label="新密码" name="newPassword">
<a-input-password
v-model:value="passwordForm.newPassword"
placeholder="请输入新密码(至少6位)"
size="large"
/>
</a-form-item>
<a-form-item label="确认新密码" name="confirmPassword">
<a-input-password
v-model:value="passwordForm.confirmPassword"
placeholder="请再次输入新密码"
size="large"
/>
</a-form-item>
<a-form-item>
<a-button
type="primary"
size="large"
@click="handleChangePassword"
:loading="changingPassword"
>
<template #icon><Icon name="check" size="sm" /></template>
修改密码
</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</div>
</div>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import type { FormInstance } from 'ant-design-vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import Icon from '@/components/Icon.vue'
import { useAuthStore } from '@/stores/auth'
import { getUserInfo } from '@/api/auth'
import { updateUser, changePassword } from '@/api/user'
import { uploadFile } from '@/api/upload'
import type { UserInfo } from '@/api/auth'
const authStore = useAuthStore()
// 表单ref
const formRef = ref<FormInstance>()
const passwordFormRef = ref<FormInstance>()
// 激活的标签页
const activeTab = ref('basic')
// 保存状态
const saving = ref(false)
const changingPassword = ref(false)
// 用户信息
const userInfo = ref<UserInfo>({
id: 0,
username: '',
realName: '',
email: '',
phone: '',
avatar: '',
deptId: 0,
deptName: '',
roles: [],
permissions: []
})
// 表单数据
const userForm = reactive({
username: '',
realName: '',
email: '',
phone: '',
avatar: ''
})
// 密码表单
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
// 表单验证规则
const formRules = {
realName: [
{ required: true, message: '请输入真实姓名', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
]
}
// 密码验证规则
const passwordRules = {
oldPassword: [
{ required: true, message: '请输入当前密码', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{
validator: (_rule: any, value: string) => {
if (value !== passwordForm.newPassword) {
return Promise.reject('两次输入的密码不一致')
}
return Promise.resolve()
},
trigger: 'blur'
}
]
}
// 加载用户信息
const loadUserInfo = async () => {
try {
const response = await getUserInfo()
// getUserInfo返回的是Result<UserInfo>,需要访问data属性
const data = response.data
userInfo.value = data
userForm.username = userInfo.value.username
userForm.realName = userInfo.value.realName || ''
userForm.email = userInfo.value.email || ''
userForm.phone = userInfo.value.phone || ''
userForm.avatar = userInfo.value.avatar || ''
} catch (error: any) {
message.error(error.message || '加载用户信息失败')
}
}
// 上传头像
const handleAvatarUpload = async (file: File) => {
try {
const response = await uploadFile(file)
userForm.avatar = response.fileUrl
message.success('头像上传成功')
return false // 阻止默认上传
} catch (error: any) {
message.error(error.message || '头像上传失败')
return false
}
}
// 删除头像
const handleRemoveAvatar = () => {
userForm.avatar = ''
message.info('头像已删除,记得保存修改')
}
// 保存个人信息
const handleSaveProfile = async () => {
try {
await formRef.value?.validate()
saving.value = true
await updateUser(userInfo.value.id, {
realName: userForm.realName,
email: userForm.email,
phone: userForm.phone,
avatar: userForm.avatar
})
// 更新authStore中的用户信息
await authStore.fetchUserInfo()
message.success('保存成功')
} catch (error: any) {
if (error.errorFields) {
// 表单验证失败
return
}
message.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
// 修改密码
const handleChangePassword = async () => {
try {
await passwordFormRef.value?.validate()
changingPassword.value = true
await changePassword(
userInfo.value.id,
passwordForm.oldPassword,
passwordForm.newPassword
)
message.success('密码修改成功,请重新登录')
// 清空密码表单
passwordForm.oldPassword = ''
passwordForm.newPassword = ''
passwordForm.confirmPassword = ''
// 延迟跳转到登录页
setTimeout(() => {
authStore.logout()
}, 1500)
} catch (error: any) {
if (error.errorFields) {
// 表单验证失败
return
}
message.error(error.message || '密码修改失败')
} finally {
changingPassword.value = false
}
}
// 组件挂载时加载用户信息
onMounted(() => {
loadUserInfo()
})
</script>
<style scoped lang="scss">
.profile-page {
padding: $space-xl 0;
min-height: calc(100vh - 64px);
background: $bg-secondary;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 $space-lg;
}
.profile-header {
margin-bottom: $space-xl;
.page-title {
font-size: $font-size-2xl;
font-weight: $font-weight-bold;
color: $text-primary;
margin: 0;
}
}
.profile-content {
.profile-card {
background: $bg-primary;
border-radius: $radius-lg;
box-shadow: $shadow-md;
}
}
.profile-form {
max-width: 600px;
margin-top: $space-lg;
}
.avatar-upload-section {
display: flex;
flex-direction: column;
gap: $space-md;
.avatar-preview {
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
border: 2px solid $border-color;
display: flex;
align-items: center;
justify-content: center;
background: $bg-tertiary;
.avatar-preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: $text-secondary;
}
}
.avatar-upload-actions {
display: flex;
gap: $space-sm;
}
.upload-tip {
font-size: $font-size-sm;
color: $text-secondary;
display: flex;
align-items: center;
}
}
:deep(.ant-tabs-card) {
.ant-tabs-tab {
border: none;
background: transparent;
color: $text-secondary;
&.ant-tabs-tab-active {
background: $bg-primary;
color: $brand-primary;
border-bottom: 2px solid $brand-primary;
}
}
.ant-tabs-content {
padding: $space-xl;
}
}
:deep(.ant-form-item-label) {
label {
font-weight: $font-weight-medium;
color: $text-primary;
}
}
:deep(.ant-input),
:deep(.ant-input-password) {
border-radius: $radius-md;
&:focus,
&:hover {
border-color: $brand-primary;
}
}
:deep(.ant-btn-primary) {
background: $brand-primary;
border-color: $brand-primary;
&:hover {
background: $brand-primary-hover;
border-color: $brand-primary-hover;
}
}
</style>