Files
schoolNews/schoolNewsWeb/src/components/base/UserDropdown.vue
2025-10-17 16:12:29 +08:00

400 lines
8.8 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>
<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>