519 lines
14 KiB
Vue
519 lines
14 KiB
Vue
<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()
|
||
// request.get返回的是Result对象,需要访问data属性
|
||
// response结构: { code: 200, data: UserInfo, message: '' }
|
||
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: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
}
|
||
|
||
.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-hover;
|
||
border-color: $brand-hover;
|
||
}
|
||
}
|
||
</style>
|