feat(personal): 新增个人中心核心功能

- 添加UserCenter用户头像下拉菜单组件
- 新增个人信息相关API接口封装
- 提供完整的个人信息管理功能接口
This commit is contained in:
Leo 2025-07-06 22:09:55 +08:00
parent 49e5a1e018
commit 2c5e2234d8
2 changed files with 176 additions and 0 deletions

View File

@ -0,0 +1,112 @@
<script setup lang="ts">
import { computed, h } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/store/auth'
import { coiMsgBox } from '@/utils/coi'
import IconUser from '~icons/icon-park-outline/user'
import IconLogout from '~icons/icon-park-outline/logout'
const authStore = useAuthStore()
const router = useRouter()
//
const userInfo = computed(() => authStore.userInfo)
//
const displayName = computed(() => {
if (!userInfo.value)
return '未知用户'
return userInfo.value.userName || '未知用户'
})
//
const avatar = computed(() => {
if (!userInfo.value?.avatar)
return ''
return userInfo.value.avatar
})
//
function handlePersonalCenter() {
router.push('/personal-center')
}
// 退
function handleLogout() {
coiMsgBox('确定要退出登录吗?', '退出确认').then(() => {
authStore.logout()
}).catch(() => {
//
})
}
</script>
<template>
<n-dropdown
placement="bottom-end"
:options="[
{
label: '个人中心',
key: 'personal-center',
icon: () => h(IconUser),
},
{
type: 'divider',
},
{
label: '退出登录',
key: 'logout',
icon: () => h(IconLogout),
},
]"
@select="(key) => {
if (key === 'personal-center') {
handlePersonalCenter()
}
else if (key === 'logout') {
handleLogout()
}
}"
>
<div class="flex items-center gap-2 cursor-pointer hover:bg-gray-50 hover:bg-opacity-80 rounded-lg px-3 py-2 transition-colors">
<!-- 用户头像 -->
<n-avatar
:size="32"
:src="avatar"
fallback-src=""
round
class="border border-gray-200"
>
<template #placeholder>
<icon-park-outline-user class="text-lg" />
</template>
</n-avatar>
<!-- 用户名称 -->
<span class="text-sm font-medium text-gray-700 max-w-20 truncate">
{{ displayName }}
</span>
<!-- 下拉箭头 -->
<icon-park-outline-down class="text-xs text-gray-500" />
</div>
</n-dropdown>
</template>
<style scoped>
.dark .hover\:bg-gray-50:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.dark .text-gray-700 {
color: #e5e7eb;
}
.dark .text-gray-500 {
color: #9ca3af;
}
.dark .border-gray-200 {
border-color: #374151;
}
</style>

View File

@ -0,0 +1,64 @@
import { request } from '../../http'
// 个人资料数据类型
export interface PersonalDataVo {
userId: number
userName: string
loginName: string
email?: string
phone?: string
sex?: string
avatar?: string
userStatus?: string
createTime?: string
}
// 修改个人资料请求类型
export interface UpdatePersonalBo {
userName?: string
email?: string
phone?: string
sex?: string
avatar?: string
}
// 修改密码请求类型
export interface UpdatePasswordBo {
password: string
newPassword: string
confirmPassword: string
}
/**
*
*/
export function getPersonalData() {
return request.Get<Service.ResponseResult<PersonalDataVo>>('/coder/sysLoginUser/getPersonalData')
}
/**
*
*/
export function updateBasicData(data: UpdatePersonalBo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/updateBasicData', data)
}
/**
*
*/
export function updatePassword(data: UpdatePasswordBo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/updateUserPwd', data)
}
/**
*
* @param file
* @param fileSize (MB)
*/
export function uploadAvatar(file: File, fileSize: number = 5) {
const formData = new FormData()
formData.append('file', file)
// 注意:不要手动设置 Content-Type让浏览器自动设置以包含正确的 boundary
// 使用数字标识符 "1" 代表头像类型,避免数据库字段长度限制
return request.Post<Service.ResponseResult<any>>(`/coder/file/uploadFile/${fileSize}/pictures/1`, formData)
}