400 lines
8.8 KiB
Vue
400 lines
8.8 KiB
Vue
<template>
|
||
<div
|
||
ref="dropdownRef"
|
||
class="user-dropdown"
|
||
@click="toggleDropdown"
|
||
@mouseenter="openDropdown"
|
||
@mouseleave="closeDropdown"
|
||
v-click-outside="forceCloseDropdown"
|
||
>
|
||
<!-- 未登录状态 -->
|
||
<div class="login-info" v-if="!isLoggedIn">
|
||
<div class="login-text">
|
||
<i class="login-icon">👤</i>
|
||
<span v-if="!collapsed">登录/注册</span>
|
||
</div>
|
||
<i class="dropdown-icon" :class="{ 'open': dropdownVisible }"></i>
|
||
</div>
|
||
|
||
<!-- 已登录状态 -->
|
||
<div class="user-info" v-else>
|
||
<div class="user-avatar">
|
||
<img :src="userAvatar" :alt="user?.username" v-if="userAvatar">
|
||
<span class="avatar-placeholder" v-else>{{ avatarText }}</span>
|
||
</div>
|
||
<div class="user-details" v-if="!collapsed">
|
||
<div class="user-name">{{ user?.realName || user?.username }}</div>
|
||
<div class="user-role">{{ primaryRole }}</div>
|
||
</div>
|
||
<i class="dropdown-icon" :class="{ 'open': dropdownVisible }"></i>
|
||
</div>
|
||
|
||
<!-- 下拉菜单 -->
|
||
<Teleport to="body">
|
||
<div
|
||
class="dropdown-menu"
|
||
:class="{ 'show': dropdownVisible }"
|
||
:style="dropdownStyle"
|
||
v-if="dropdownVisible"
|
||
@mouseenter="openDropdown"
|
||
@mouseleave="closeDropdown"
|
||
>
|
||
<!-- 未登录时的菜单 -->
|
||
<template v-if="!isLoggedIn">
|
||
<div class="dropdown-item" @click="goToLogin">
|
||
<i class="item-icon icon-login"></i>
|
||
<span>登录</span>
|
||
</div>
|
||
<div class="dropdown-item" @click="goToRegister">
|
||
<i class="item-icon icon-register"></i>
|
||
<span>注册</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 已登录时的菜单 -->
|
||
<template v-else>
|
||
<!-- 动态菜单项(个人中心、账号中心) -->
|
||
<div
|
||
v-for="menu in userMenus"
|
||
:key="menu.menuID"
|
||
class="dropdown-item"
|
||
@click="goToMenu(menu)"
|
||
>
|
||
<i class="item-icon">{{ getMenuIcon(menu) }}</i>
|
||
<span>{{ menu.name }}</span>
|
||
</div>
|
||
<div class="dropdown-divider" v-if="userMenus.length > 0"></div>
|
||
<div class="dropdown-item danger" @click="handleLogout">
|
||
<i class="item-icon icon-logout"></i>
|
||
<span>退出登录</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { useStore } from 'vuex';
|
||
import type { UserVO, SysMenu } from '@/types';
|
||
|
||
// Props
|
||
interface Props {
|
||
user?: UserVO | null;
|
||
collapsed?: boolean;
|
||
}
|
||
|
||
const props = defineProps<Props>();
|
||
|
||
// Emits
|
||
const emit = defineEmits<{
|
||
logout: [];
|
||
}>();
|
||
|
||
// 状态
|
||
const dropdownVisible = ref(false);
|
||
const dropdownRef = ref<HTMLElement | null>(null);
|
||
const dropdownPosition = ref({ top: 0, left: 0 });
|
||
const closeTimer = ref<number | null>(null);
|
||
|
||
// Composition API
|
||
const router = useRouter();
|
||
const store = useStore();
|
||
|
||
// 计算属性
|
||
const isLoggedIn = computed(() => {
|
||
return store.getters['auth/isAuthenticated'];
|
||
});
|
||
|
||
const userAvatar = computed(() => {
|
||
return props.user?.avatar || '';
|
||
});
|
||
|
||
const avatarText = computed(() => {
|
||
const name = props.user?.realName || props.user?.username || '';
|
||
return name.charAt(0).toUpperCase();
|
||
});
|
||
|
||
const primaryRole = computed(() => {
|
||
// 这里可以从store中获取用户角色信息
|
||
return '管理员'; // 暂时硬编码
|
||
});
|
||
|
||
// 获取用户下拉菜单(个人中心和账号中心)
|
||
const userMenus = computed(() => {
|
||
const allMenus = store.getters['auth/menuTree'] as SysMenu[];
|
||
|
||
// 查找"用户下拉菜单"容器
|
||
const userDropdownMenu = allMenus.find((menu: SysMenu) => menu.menuID === 'menu_user_dropdown');
|
||
|
||
if (!userDropdownMenu || !userDropdownMenu.children) {
|
||
return [];
|
||
}
|
||
|
||
// 返回用户下拉菜单下的所有NAVIGATION类型子菜单(个人中心、账号中心)
|
||
return userDropdownMenu.children.filter((menu: SysMenu) => menu.type === 1); // MenuType.NAVIGATION = 1
|
||
});
|
||
|
||
// 下拉菜单样式
|
||
const dropdownStyle = computed(() => {
|
||
return {
|
||
position: 'fixed' as const,
|
||
top: `${dropdownPosition.value.top}px`,
|
||
left: `${dropdownPosition.value.left}px`,
|
||
minWidth: '160px',
|
||
};
|
||
});
|
||
|
||
// 方法 - 使用 function 声明
|
||
function openDropdown() {
|
||
// 清除关闭定时器
|
||
if (closeTimer.value) {
|
||
clearTimeout(closeTimer.value);
|
||
closeTimer.value = null;
|
||
}
|
||
|
||
dropdownVisible.value = true;
|
||
|
||
// 计算下拉菜单位置
|
||
if (dropdownRef.value) {
|
||
const rect = dropdownRef.value.getBoundingClientRect();
|
||
dropdownPosition.value = {
|
||
top: rect.bottom + 4, // 距离触发器底部4px
|
||
left: rect.right - 160, // 右对齐:触发器右边缘减去下拉菜单宽度
|
||
};
|
||
}
|
||
}
|
||
|
||
function toggleDropdown() {
|
||
if (dropdownVisible.value) {
|
||
forceCloseDropdown();
|
||
} else {
|
||
openDropdown();
|
||
}
|
||
}
|
||
|
||
function closeDropdown() {
|
||
// 延迟关闭,给用户时间移动到下拉菜单
|
||
closeTimer.value = setTimeout(() => {
|
||
dropdownVisible.value = false;
|
||
closeTimer.value = null;
|
||
}, 200);
|
||
}
|
||
|
||
function forceCloseDropdown() {
|
||
if (closeTimer.value) {
|
||
clearTimeout(closeTimer.value);
|
||
closeTimer.value = null;
|
||
}
|
||
dropdownVisible.value = false;
|
||
}
|
||
|
||
// 未登录时的操作
|
||
function goToLogin() {
|
||
forceCloseDropdown();
|
||
router.push('/login');
|
||
}
|
||
|
||
function goToRegister() {
|
||
forceCloseDropdown();
|
||
router.push('/register');
|
||
}
|
||
|
||
// 已登录时的操作
|
||
function goToMenu(menu: SysMenu) {
|
||
forceCloseDropdown();
|
||
if (menu.url) {
|
||
router.push(menu.url);
|
||
}
|
||
}
|
||
|
||
function getMenuIcon(menu: SysMenu) {
|
||
// 根据菜单ID返回对应图标
|
||
if (menu.menuID === 'menu_user_center') {
|
||
return '👤';
|
||
} else if (menu.menuID === 'menu_profile') {
|
||
return '⚙️';
|
||
}
|
||
return '📋';
|
||
}
|
||
|
||
function handleLogout() {
|
||
forceCloseDropdown();
|
||
emit('logout');
|
||
}
|
||
|
||
// 点击外部关闭指令
|
||
const vClickOutside = {
|
||
mounted(el: any, binding: any) {
|
||
el._clickOutside = (event: Event) => {
|
||
if (!(el === event.target || el.contains(event.target as Node))) {
|
||
binding.value();
|
||
}
|
||
};
|
||
document.addEventListener('click', el._clickOutside);
|
||
},
|
||
unmounted(el: any) {
|
||
document.removeEventListener('click', el._clickOutside);
|
||
delete el._clickOutside;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.user-dropdown {
|
||
color: #ffffff;
|
||
position: relative;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.login-info,
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s ease;
|
||
|
||
&:hover {
|
||
background-color: #f5f5f5;
|
||
color: #000000;
|
||
}
|
||
}
|
||
|
||
.login-text {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.login-icon {
|
||
margin-right: 8px;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
overflow: hidden;
|
||
margin-right: 12px;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.avatar-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.user-details {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.user-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
line-height: 1.2;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.user-role {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-top: 2px;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
|
||
.dropdown-icon {
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
transition: transform 0.2s ease;
|
||
|
||
&::before {
|
||
content: "▼";
|
||
}
|
||
|
||
&.open {
|
||
transform: rotate(180deg);
|
||
}
|
||
}
|
||
|
||
.dropdown-menu {
|
||
background: white;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
border: 1px solid #e8e8e8;
|
||
z-index: 10000;
|
||
overflow: hidden;
|
||
opacity: 0;
|
||
transform: scaleY(0);
|
||
transform-origin: top center;
|
||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||
|
||
&.show {
|
||
opacity: 1;
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
|
||
.dropdown-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
font-size: 14px;
|
||
color: #333;
|
||
|
||
&:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
&.danger {
|
||
color: #ff4d4f;
|
||
|
||
&:hover {
|
||
background-color: #fff2f0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.item-icon {
|
||
width: 16px;
|
||
margin-right: 12px;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.dropdown-divider {
|
||
height: 1px;
|
||
background-color: #e8e8e8;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
/* 图标字体类 */
|
||
.icon-login::before { content: "🔑"; }
|
||
.icon-register::before { content: "📝"; }
|
||
.icon-profile::before { content: "👤"; }
|
||
.icon-settings::before { content: "⚙️"; }
|
||
.icon-logout::before { content: "🚪"; }
|
||
</style>
|