ForcePilot/web/src/components/UserInfoComponent.vue

565 lines
14 KiB
Vue
Raw Normal View History

2025-05-02 23:56:59 +08:00
<template>
<div class="user-info-component">
<a-dropdown :trigger="['click']" v-if="userStore.isLoggedIn">
2025-05-04 21:04:01 +08:00
<div class="user-info-dropdown" :data-align="showRole ? 'left' : 'center'">
<div class="user-avatar">
<img v-if="userStore.avatar" :src="userStore.avatar" :alt="userStore.username" class="avatar-image" />
<CircleUser v-else />
<!-- <div class="user-role-badge" :class="userRoleClass"></div> -->
2025-05-04 21:04:01 +08:00
</div>
<div v-if="showRole">{{ userStore.username }}</div>
2025-05-02 23:56:59 +08:00
</div>
<template #overlay>
2025-05-04 21:04:01 +08:00
<a-menu>
2025-05-02 23:56:59 +08:00
<a-menu-item key="username" disabled>
<span class="user-menu-username">{{ userStore.username }}</span>
</a-menu-item>
<a-menu-item key="user-id" disabled>
<span class="user-menu-info">ID: {{ userStore.userIdLogin }}</span>
</a-menu-item>
2025-05-02 23:56:59 +08:00
<a-menu-item key="role" disabled>
<span class="user-menu-role">{{ userRoleText }}</span>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="profile" @click="openProfile">
<UserOutlined /> &nbsp;个人资料
</a-menu-item>
2025-05-02 23:56:59 +08:00
<a-menu-item key="logout" @click="logout">
<LogoutOutlined /> &nbsp;退出登录
2025-05-02 23:56:59 +08:00
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-button v-else-if="showButton" type="primary" @click="goToLogin">
登录
2025-05-02 23:56:59 +08:00
</a-button>
<div v-else class="login-icon" @click="goToLogin">
<UserRoundCheck />
</div>
<!-- 个人资料弹窗 -->
<a-modal
v-model:open="profileModalVisible"
title="个人资料"
:footer="null"
width="520px"
class="profile-modal"
>
<div class="profile-content">
<!-- 头像区域 -->
<div class="avatar-section">
<div class="avatar-container">
<div class="avatar-display">
<img v-if="userStore.avatar" :src="userStore.avatar" :alt="userStore.username" class="large-avatar" />
<div v-else class="default-avatar">
<CircleUser :size="60" />
</div>
</div>
<div class="avatar-actions">
<a-upload
:show-upload-list="false"
:before-upload="beforeUpload"
@change="handleAvatarChange"
accept="image/*"
>
<a-button type="primary" size="small" :loading="avatarUploading">
<template #icon><UploadOutlined /></template>
{{ userStore.avatar ? '更换头像' : '上传头像' }}
</a-button>
</a-upload>
<div class="avatar-tips">
支持 JPGPNG 格式文件不超过 5MB
</div>
</div>
</div>
</div>
<!-- 用户信息区域 -->
<div class="info-section">
<div class="info-item">
<div class="info-label">用户名</div>
<div class="info-value" v-if="!profileEditing">{{ userStore.username || '未设置' }}</div>
<div class="info-value" v-else>
<a-input
v-model:value="editedProfile.username"
placeholder="请输入用户名2-20个字符"
:max-length="20"
style="width: 240px;"
/>
</div>
</div>
<div class="info-item">
<div class="info-label">用户ID</div>
<div class="info-value user-id" v-if="!profileEditing">{{ userStore.userIdLogin || '未设置' }}</div>
<div class="info-value" v-else>
<a-input
:value="userStore.userIdLogin || ''"
disabled
style="width: 240px;"
/>
</div>
</div>
<div class="info-item">
<div class="info-label">手机号</div>
<div class="info-value" v-if="!profileEditing">
{{ userStore.phoneNumber || '未设置' }}
</div>
<div class="info-value" v-else>
<a-input
v-model:value="editedProfile.phone_number"
placeholder="请输入手机号"
:max-length="11"
style="width: 200px;"
/>
</div>
</div>
<div class="info-item">
<div class="info-label">角色</div>
<div class="info-value">
<a-tag :color="getRoleColor(userStore.userRole)" class="role-tag">
{{ userRoleText }}
</a-tag>
</div>
</div>
</div>
<!-- 操作区域 -->
<div class="actions-section">
<a-space>
<template v-if="!profileEditing">
<a-button type="primary" @click="startEdit">
编辑资料
</a-button>
<a-button @click="profileModalVisible = false">
关闭
</a-button>
</template>
<template v-else>
<a-button type="primary" @click="saveProfile" :loading="avatarUploading">
保存
</a-button>
<a-button @click="cancelEdit">
取消
</a-button>
</template>
</a-space>
</div>
</div>
</a-modal>
2025-05-02 23:56:59 +08:00
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
2025-05-02 23:56:59 +08:00
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { UserOutlined, LogoutOutlined, UploadOutlined } from '@ant-design/icons-vue';
2025-05-02 23:56:59 +08:00
import { message } from 'ant-design-vue';
import { CircleUser, UserRoundCheck } from 'lucide-vue-next';
2025-05-02 23:56:59 +08:00
const router = useRouter();
const userStore = useUserStore();
// 个人资料弹窗状态
const profileModalVisible = ref(false);
const avatarUploading = ref(false);
const profileEditing = ref(false);
const editedProfile = ref({
username: '',
phone_number: ''
});
2025-05-04 21:04:01 +08:00
const props = defineProps({
showRole: {
type: Boolean,
default: false
},
showButton: {
type: Boolean,
default: false
2025-05-04 21:04:01 +08:00
}
})
2025-05-02 23:56:59 +08:00
// 用户名首字母(用于显示在头像中)
const userInitial = computed(() => {
if (!userStore.username) return '?';
return userStore.username.charAt(0).toUpperCase();
});
// 用户角色显示文本
const userRoleText = computed(() => {
switch (userStore.userRole) {
case 'superadmin':
return '超级管理员';
case 'admin':
return '管理员';
case 'user':
return '普通用户';
default:
return '未知角色';
}
});
// 用户角色徽章样式类
const userRoleClass = computed(() => {
return {
'superadmin': userStore.userRole === 'superadmin',
'admin': userStore.userRole === 'admin',
'user': userStore.userRole === 'user'
};
});
// 退出登录
const logout = () => {
userStore.logout();
message.success('已退出登录');
// 跳转到首页
router.push('/login');
};
// 前往登录页
const goToLogin = () => {
router.push('/login');
};
// 打开个人资料页面
const openProfile = async () => {
profileModalVisible.value = true;
profileEditing.value = false;
// 刷新用户信息并初始化编辑表单
try {
await userStore.getCurrentUser();
editedProfile.value = {
username: userStore.username || '',
phone_number: userStore.phoneNumber || ''
};
} catch (error) {
console.error('刷新用户信息失败:', error);
}
};
// 角色标签颜色
const getRoleColor = (role) => {
switch (role) {
case 'superadmin': return 'red';
case 'admin': return 'blue';
case 'user': return 'green';
default: return 'default';
}
};
// 开始编辑个人资料
const startEdit = () => {
profileEditing.value = true;
editedProfile.value = {
username: userStore.username || '',
phone_number: userStore.phoneNumber || ''
};
};
// 取消编辑
const cancelEdit = () => {
profileEditing.value = false;
editedProfile.value = {
username: userStore.username || '',
phone_number: userStore.phoneNumber || ''
};
};
// 保存个人资料
const saveProfile = async () => {
try {
// 验证用户名
if (editedProfile.value.username && (editedProfile.value.username.trim().length < 2 || editedProfile.value.username.trim().length > 20)) {
message.error('用户名长度必须在 2-20 个字符之间');
return;
}
// 验证手机号格式
if (editedProfile.value.phone_number && !validatePhoneNumber(editedProfile.value.phone_number)) {
message.error('请输入正确的手机号格式');
return;
}
await userStore.updateProfile({
username: editedProfile.value.username?.trim() || undefined,
phone_number: editedProfile.value.phone_number || undefined,
});
message.success('个人资料更新成功!');
profileEditing.value = false;
} catch (error) {
console.error('更新个人资料失败:', error);
message.error('更新失败:' + (error.message || '请稍后重试'));
}
};
// 手机号验证
const validatePhoneNumber = (phone) => {
if (!phone) return true; // 空手机号允许
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
};
// 头像上传前验证
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片大小不能超过 5MB');
return false;
}
return true;
};
// 处理头像上传
const handleAvatarChange = async (info) => {
if (info.file.status === 'uploading') {
avatarUploading.value = true;
return;
}
if (info.file.status === 'done') {
avatarUploading.value = false;
return;
}
// 手动处理文件上传
try {
avatarUploading.value = true;
const result = await userStore.uploadAvatar(info.file.originFileObj || info.file);
message.success('头像上传成功!');
} catch (error) {
console.error('头像上传失败:', error);
message.error('头像上传失败:' + (error.message || '请稍后重试'));
} finally {
avatarUploading.value = false;
}
};
2025-05-02 23:56:59 +08:00
</script>
<style lang="less" scoped>
.user-info-component {
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-800);
2025-05-04 21:04:01 +08:00
// margin-bottom: 16px;
}
.user-info-dropdown {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
&[data-align="center"] {
justify-content: center;
}
&[data-align="left"] {
justify-content: flex-start;
}
2025-05-02 23:56:59 +08:00
}
.user-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 18px;
cursor: pointer;
position: relative;
overflow: hidden;
2025-05-02 23:56:59 +08:00
&:hover {
opacity: 0.9;
}
.avatar-image {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 50%;
border: 2px solid #f0f0f0;
}
2025-05-02 23:56:59 +08:00
}
.user-role-badge {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
right: 0;
bottom: 0;
border: 2px solid white;
&.superadmin {
background-color: #c1bd00; // 红色,超管
}
&.admin {
background-color: #1890ff; // 蓝色,管理员
}
&.user {
background-color: #52c41a; // 绿色,普通用户
}
}
.user-menu-username {
font-weight: bold;
}
.user-menu-info {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
2025-05-02 23:56:59 +08:00
.user-menu-role {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.login-icon {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.3s;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
.profile-modal {
:deep(.ant-modal-header) {
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
.ant-modal-title {
font-size: 18px;
font-weight: 600;
color: #262626;
}
}
:deep(.ant-modal-body) {
padding: 24px;
}
}
.profile-content {
.avatar-section {
text-align: center;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
.avatar-container {
display: inline-block;
.avatar-display {
margin-bottom: 16px;
.large-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.default-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: #f5f5f5;
display: flex;
margin: 0 auto;
align-items: center;
justify-content: center;
border: 3px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// 确保图标居中
:deep(svg) {
color: #bfbfbf;
}
}
}
.avatar-actions {
.avatar-tips {
margin-top: 8px;
font-size: 12px;
color: #8c8c8c;
line-height: 1.4;
}
}
}
}
.info-section {
margin-bottom: 24px;
.info-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.info-label {
width: 80px;
font-weight: 500;
color: #8c8c8c;
flex-shrink: 0;
}
.info-value {
flex: 1;
color: #262626;
font-size: 14px;
&.user-id {
font-family: 'Monaco', 'Consolas', monospace;
// background: #f5f5f5;
// padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
}
.role-tag {
font-weight: 500;
border-radius: 4px;
padding: 4px 12px;
}
}
}
.actions-section {
text-align: center;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
}
2025-05-02 23:56:59 +08:00
</style>