feat(personal): 全面重构个人中心页面UI和功能

- 重新设计页面布局,移除顶部标题区域
- 优化个人信息展示,调整字段排序和显示位置
- 新增角色信息显示,支持多角色标签展示
- 修复状态标签颜色显示逻辑,启用状态使用主题色
- 使用n-tabs组件重新设计右侧操作区域
- 集成基本资料编辑和密码修改为标签页形式
- 优化日期格式显示为YYYY-MM-DD HH:mm:ss格式
- 改进表单验证错误处理机制
- 调整操作按钮位置到表单下方左侧
- 移除弹框设计,改为内联表单编辑

主要改进:
- 统一视觉体验和交互逻辑
- 更好的空间利用和布局优化
- 增强的表单验证用户体验
- 清晰的功能分组和操作流程
This commit is contained in:
Leo 2025-07-07 15:54:08 +08:00
parent b8ee337a75
commit 8f8a416e88

View File

@ -2,16 +2,6 @@
<div class="p-1 bg-gray-50 h-screen flex flex-col"> <div class="p-1 bg-gray-50 h-screen flex flex-col">
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 overflow-auto"> <div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 overflow-auto">
<div class="p-6"> <div class="p-6">
<!-- 页面标题 -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">
个人中心
</h1>
<p class="text-gray-600 mt-1">
管理您的个人信息和账户设置
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左侧个人信息卡片 --> <!-- 左侧个人信息卡片 -->
<div class="lg:col-span-1"> <div class="lg:col-span-1">
@ -56,48 +46,68 @@
@change="handleAvatarChange" @change="handleAvatarChange"
> >
<!-- 用户名 -->
<h3 class="text-xl font-semibold mt-4 text-gray-900">
{{ personalData.userName || '未设置' }}
</h3>
<!-- 登录名 -->
<p class="text-gray-500 text-sm">
@{{ personalData.loginName }}
</p>
<!-- 头像上传提示 --> <!-- 头像上传提示 -->
<p class="text-xs text-gray-400 mt-2"> <p class="text-xs text-gray-400 mt-4">
点击头像上传新头像 点击头像上传新头像
</p> </p>
</div> </div>
<!-- 详细信息 --> <!-- 详细信息 -->
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between items-center py-2 border-b border-gray-100"> <div class="flex justify-between items-center py-2">
<span class="text-gray-600">登录账号</span>
<span class="text-gray-900 text-sm">{{ personalData.loginName || '未设置' }}</span>
</div>
<div class="flex justify-between items-center py-2">
<span class="text-gray-600">用户姓名</span>
<span class="text-gray-900 text-sm">{{ personalData.userName || '未设置' }}</span>
</div>
<div class="flex justify-between items-center py-2">
<span class="text-gray-600">用户角色</span>
<div>
<n-tag
v-for="(roleName, index) in getRoleNames()"
:key="index"
type="primary"
size="small"
class="mr-1"
>
{{ roleName }}
</n-tag>
<span v-if="getRoleNames().length === 0" class="text-gray-500">未分配角色</span>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">邮箱</span> <span class="text-gray-600">邮箱</span>
<span class="text-gray-900">{{ personalData.email || '未设置' }}</span> <span class="text-gray-900">{{ personalData.email || '未设置' }}</span>
</div> </div>
<div class="flex justify-between items-center py-2 border-b border-gray-100"> <div class="flex justify-between items-center py-2">
<span class="text-gray-600">手机号</span> <span class="text-gray-600">手机号</span>
<span class="text-gray-900">{{ personalData.phone || '未设置' }}</span> <span class="text-gray-900">{{ personalData.phone || '未设置' }}</span>
</div> </div>
<div class="flex justify-between items-center py-2 border-b border-gray-100"> <div class="flex justify-between items-center py-2">
<span class="text-gray-600">性别</span> <span class="text-gray-600">性别</span>
<span class="text-gray-900">{{ getGenderText(personalData.sex || '3') }}</span> <span class="text-gray-900">{{ getGenderText(personalData.sex || '3') }}</span>
</div> </div>
<div class="flex justify-between items-center py-2 border-b border-gray-100"> <div class="flex justify-between items-center py-2">
<span class="text-gray-600">状态</span> <span class="text-gray-600">状态</span>
<n-tag :type="personalData.userStatus === '0' ? 'success' : 'error'" size="small"> <n-tag
:type="(personalData.userStatus || '0') === '0' ? 'info' : 'error'"
:style="(personalData.userStatus || '0') === '0' ? { backgroundColor: '#6366f1', color: 'white', border: 'none' } : {}"
size="small"
>
{{ getStatusText(personalData.userStatus || '0') }} {{ getStatusText(personalData.userStatus || '0') }}
</n-tag> </n-tag>
</div> </div>
<div class="flex justify-between items-center py-2"> <div class="flex justify-between items-center py-2">
<span class="text-gray-600">注册时间</span> <span class="text-gray-600">创建日期</span>
<span class="text-gray-900 text-sm">{{ formatDate(personalData.createTime || '') }}</span> <span class="text-gray-900 text-sm">{{ formatDate(personalData.createTime || '') }}</span>
</div> </div>
</div> </div>
@ -106,148 +116,155 @@
</div> </div>
<!-- 右侧操作区域 --> <!-- 右侧操作区域 -->
<div class="lg:col-span-2 space-y-6"> <div class="lg:col-span-2">
<!-- 基本资料编辑 --> <n-card>
<n-card title="基本资料编辑"> <n-tabs type="line" animated>
<template #header-extra> <!-- 基本资料 -->
<n-space> <n-tab-pane name="basic" tab="基本资料">
<n-button :loading="loading" @click="handleBasicReset"> <div class="pt-4">
<template #icon> <!-- 基本资料表单 -->
<icon-park-outline-refresh /> <n-form
</template> ref="basicFormRef"
重置 :model="basicForm"
</n-button> :rules="basicRules"
<n-button type="primary" :loading="loading" @click="handleBasicSave"> label-placement="left"
<template #icon> label-width="100px"
<icon-park-outline-save /> require-mark-placement="right-hanging"
</template> >
保存 <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
</n-button> <n-form-item label="用户名" path="userName">
</n-space> <n-input v-model:value="basicForm.userName" placeholder="请输入用户名" />
</template> </n-form-item>
<n-form <n-form-item label="性别" path="sex">
ref="basicFormRef" <n-select
:model="basicForm" v-model:value="basicForm.sex"
:rules="basicRules" :options="genderOptions"
label-placement="left" placeholder="请选择性别"
label-width="100px" />
require-mark-placement="right-hanging" </n-form-item>
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<n-form-item label="用户名" path="userName">
<n-input v-model:value="basicForm.userName" placeholder="请输入用户名" />
</n-form-item>
<n-form-item label="性别" path="sex"> <n-form-item label="邮箱地址" path="email">
<n-select <n-input v-model:value="basicForm.email" placeholder="请输入邮箱地址" />
v-model:value="basicForm.sex" </n-form-item>
:options="genderOptions"
placeholder="请选择性别"
/>
</n-form-item>
<n-form-item label="邮箱地址" path="email"> <n-form-item label="手机号码" path="phone">
<n-input v-model:value="basicForm.email" placeholder="请输入邮箱地址" /> <n-input v-model:value="basicForm.phone" placeholder="请输入手机号码" />
</n-form-item> </n-form-item>
</div>
</n-form>
<n-form-item label="手机号码" path="phone"> <!-- 操作按钮 -->
<n-input v-model:value="basicForm.phone" placeholder="请输入手机号码" /> <div class="flex justify-start mt-6">
</n-form-item> <n-space>
</div> <n-button :loading="loading" @click="handleBasicReset">
</n-form> <template #icon>
</n-card> <icon-park-outline-refresh />
</template>
<!-- 密码管理 --> 重置
<n-card title="密码管理"> </n-button>
<template #header-extra> <n-button type="primary" :loading="loading" @click="handleBasicSave">
<n-button type="warning" @click="openPasswordDialog"> <template #icon>
<template #icon> <icon-park-outline-save />
<icon-park-outline-lock /> </template>
</template> 保存
修改密码 </n-button>
</n-button> </n-space>
</template> </div>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div class="flex items-start">
<icon-park-outline-info class="text-yellow-600 text-lg mt-0.5 mr-3" />
<div>
<h4 class="text-yellow-800 font-medium">
密码安全提示
</h4>
<p class="text-yellow-700 text-sm mt-1">
为了您的账户安全建议定期更换密码新密码应包含字母数字长度不少于6位
</p>
</div> </div>
</div> </n-tab-pane>
</div>
<!-- 修改密码 -->
<n-tab-pane name="password" tab="修改密码">
<div class="pt-4">
<!-- 密码安全提示 -->
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div class="flex items-start">
<icon-park-outline-info class="text-yellow-600 text-lg mt-0.5 mr-3" />
<div>
<h4 class="text-yellow-800 font-medium">
密码安全提示
</h4>
<p class="text-yellow-700 text-sm mt-1">
为了您的账户安全建议定期更换密码新密码应包含字母数字长度不少于6位
</p>
</div>
</div>
</div>
<!-- 密码修改表单 -->
<n-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
label-placement="left"
label-width="100px"
require-mark-placement="right-hanging"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<n-form-item label="当前密码" path="password">
<n-input
v-model:value="passwordForm.password"
type="password"
show-password-on="click"
placeholder="请输入当前密码"
/>
</n-form-item>
<n-form-item label="新密码" path="newPassword">
<n-input
v-model:value="passwordForm.newPassword"
type="password"
show-password-on="click"
placeholder="请输入新密码"
/>
</n-form-item>
<n-form-item label="确认新密码" path="confirmPassword" class="md:col-span-2">
<n-input
v-model:value="passwordForm.confirmPassword"
type="password"
show-password-on="click"
placeholder="请再次输入新密码"
/>
</n-form-item>
</div>
</n-form>
<!-- 操作按钮 -->
<div class="flex justify-start mt-6">
<n-space>
<n-button @click="handlePasswordReset">
<template #icon>
<icon-park-outline-refresh />
</template>
重置
</n-button>
<n-button type="warning" :loading="loading" @click="handlePasswordSubmit">
<template #icon>
<icon-park-outline-lock />
</template>
修改密码
</n-button>
</n-space>
</div>
</div>
</n-tab-pane>
</n-tabs>
</n-card> </n-card>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 修改密码弹框 -->
<NovaDialog
ref="passwordDialogRef"
title="修改密码"
:width="500"
height="auto"
confirm-text="确认修改"
cancel-text="取消"
@nova-confirm="handlePasswordSubmit"
@nova-cancel="handlePasswordCancel"
>
<template #content>
<div class="px-3 py-2">
<n-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
label-placement="top"
require-mark-placement="right-hanging"
>
<n-form-item label="当前密码" path="password">
<n-input
v-model:value="passwordForm.password"
type="password"
show-password-on="click"
placeholder="请输入当前密码"
/>
</n-form-item>
<n-form-item label="新密码" path="newPassword">
<n-input
v-model:value="passwordForm.newPassword"
type="password"
show-password-on="click"
placeholder="请输入新密码"
/>
</n-form-item>
<n-form-item label="确认新密码" path="confirmPassword">
<n-input
v-model:value="passwordForm.confirmPassword"
type="password"
show-password-on="click"
placeholder="请再次输入新密码"
/>
</n-form-item>
</n-form>
</div>
</template>
</NovaDialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
import { coiMsgError, coiMsgSuccess } from '@/utils/coi' import { coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { getPersonalData, updateBasicData, updatePassword, uploadAvatar } from '@/service/api/personal' import { getPersonalData, updateBasicData, updatePassword, uploadAvatar } from '@/service/api/personal'
import type { PersonalDataVo, UpdatePasswordBo, UpdatePersonalBo } from '@/service/api/personal' import type { PersonalDataVo, UpdatePasswordBo, UpdatePersonalBo } from '@/service/api/personal'
import NovaDialog from '@/components/common/NovaDialog.vue'
import { serviceConfig } from '@/../service.config' import { serviceConfig } from '@/../service.config'
// //
@ -266,6 +283,8 @@ const personalData = ref<PersonalDataVo>({
avatar: '', avatar: '',
userStatus: '0', userStatus: '0',
createTime: '', createTime: '',
roleNames: [],
roleName: '',
}) })
// //
@ -289,9 +308,6 @@ const passwordForm = ref<UpdatePasswordBo>({
confirmPassword: '', confirmPassword: '',
}) })
//
const passwordDialogRef = ref()
// //
const basicRules = { const basicRules = {
userName: [ userName: [
@ -352,6 +368,17 @@ function getStatusText(status: string) {
return option?.label || '未知' return option?.label || '未知'
} }
//
function getRoleNames(): string[] {
if (personalData.value.roleNames && personalData.value.roleNames.length > 0) {
return personalData.value.roleNames
}
if (personalData.value.roleName) {
return [personalData.value.roleName]
}
return []
}
// //
async function fetchPersonalData() { async function fetchPersonalData() {
try { try {
@ -451,8 +478,21 @@ async function handleAvatarSave() {
// //
async function handleBasicSave() { async function handleBasicSave() {
if (!basicFormRef.value)
return
try {
//
await basicFormRef.value.validate()
}
catch {
//
coiMsgWarning('验证失败,请检查填写内容')
return
}
// API
try { try {
await basicFormRef.value?.validate()
loading.value = true loading.value = true
const result = await updateBasicData(basicForm.value) const result = await updateBasicData(basicForm.value)
if (result.isSuccess) { if (result.isSuccess) {
@ -487,25 +527,38 @@ function handleBasicReset() {
} }
} }
// //
function openPasswordDialog() { function handlePasswordReset() {
passwordForm.value = { passwordForm.value = {
password: '', password: '',
newPassword: '', newPassword: '',
confirmPassword: '', confirmPassword: '',
} }
passwordDialogRef.value?.novaOpen()
} }
// //
async function handlePasswordSubmit() { async function handlePasswordSubmit() {
if (!passwordFormRef.value)
return
try {
//
await passwordFormRef.value.validate()
}
catch {
//
coiMsgWarning('验证失败,请检查填写内容')
return
}
// API
try { try {
await passwordFormRef.value?.validate()
loading.value = true loading.value = true
const result = await updatePassword(passwordForm.value) const result = await updatePassword(passwordForm.value)
if (result.isSuccess) { if (result.isSuccess) {
coiMsgSuccess('密码修改成功') coiMsgSuccess('密码修改成功')
passwordDialogRef.value?.novaClose() //
handlePasswordReset()
} }
else { else {
coiMsgError('密码修改失败') coiMsgError('密码修改失败')
@ -519,16 +572,20 @@ async function handlePasswordSubmit() {
} }
} }
//
function handlePasswordCancel() {
passwordDialogRef.value?.novaClose()
}
// //
function formatDate(dateStr: string) { function formatDate(dateStr: string) {
if (!dateStr) if (!dateStr)
return '-' return '-'
return new Date(dateStr).toLocaleString('zh-CN')
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} }
// //