添加个人中心页面:创建Profile.vue,添加路由,修复Navbar个人信息菜单点击事件
This commit is contained in:
parent
a6ebea2438
commit
8b599d2226
@ -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')
|
||||
|
||||
@ -120,6 +120,14 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '我的目标'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('@/views/Profile.vue'),
|
||||
meta: {
|
||||
title: '个人中心'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/team-pk',
|
||||
name: 'TeamPK',
|
||||
|
||||
517
frontend/src/views/Profile.vue
Normal file
517
frontend/src/views/Profile.vue
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user