519 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>