456 lines
16 KiB
JavaScript
456 lines
16 KiB
JavaScript
class UserInfo extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
this.attachShadow({ mode: 'open' });
|
||
}
|
||
|
||
connectedCallback() {
|
||
this.render();
|
||
this.addEventListeners();
|
||
this.checkLoginStatus();
|
||
}
|
||
|
||
render() {
|
||
this.shadowRoot.innerHTML = `
|
||
<style>
|
||
.user-dropdown {
|
||
height: 100%;
|
||
position: relative;
|
||
border-left: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.user-dropdown-toggle {
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 20px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
gap: 10px;
|
||
position: relative;
|
||
}
|
||
|
||
.user-dropdown-toggle:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.user-dropdown-toggle img.icon-small {
|
||
width: 12px;
|
||
height: 12px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.user-dropdown-toggle img:not(.icon-small) {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.user-dropdown-toggle span {
|
||
font-size: 17px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.user-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
margin-right: 0;
|
||
vertical-align: middle;
|
||
cursor: help;
|
||
}
|
||
|
||
.user-icon-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
z-index: 1002;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.icon-small {
|
||
width: 12px;
|
||
height: 12px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.user-dropdown-menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
background: white;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
display: none;
|
||
z-index: 1000;
|
||
min-width: 150px;
|
||
max-width: 250px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.user-dropdown.active .user-dropdown-menu {
|
||
display: block;
|
||
}
|
||
|
||
.dropdown-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.dropdown-item:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.dropdown-item .icon-small {
|
||
width: 16px;
|
||
height: 16px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.dropdown-divider {
|
||
height: 1px;
|
||
background-color: #e0e0e0;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.tooltip {
|
||
position: absolute;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
top: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
margin-top: 8px;
|
||
z-index: 1001;
|
||
min-width: 200px;
|
||
text-align: left;
|
||
line-height: 1.4;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.tooltip.right-aligned {
|
||
left: auto;
|
||
right: 0;
|
||
transform: none;
|
||
margin: 8px 0 0 0;
|
||
padding: 8px 12px 8px 12px;
|
||
}
|
||
|
||
.tooltip .info-row {
|
||
margin: 2px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tooltip .label {
|
||
color: #a8c8ff;
|
||
flex-shrink: 0;
|
||
min-width: 70px;
|
||
}
|
||
|
||
.tooltip .value {
|
||
color: #ffffff;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.user-icon-wrapper:hover .tooltip {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
.tooltip::before {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
border-width: 0 5px 5px 5px;
|
||
border-style: solid;
|
||
border-color: transparent transparent rgba(0, 0, 0, 0.8) transparent;
|
||
}
|
||
|
||
.dropdown-item span {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
</style>
|
||
<div class="user-dropdown">
|
||
<div class="user-dropdown-toggle">
|
||
<div class="user-icon-wrapper">
|
||
<img id="userIcon" src="assets/icons/png/user.png" alt="用户" class="user-icon">
|
||
<div class="tooltip" id="userTooltip">用户信息加载中...</div>
|
||
</div>
|
||
<span id="userName">用户名</span>
|
||
<img src="assets/icons/png/chevron-down_b.png" alt="展开" class="icon-small">
|
||
</div>
|
||
<div class="user-dropdown-menu">
|
||
<div class="dropdown-item" data-action="profile">
|
||
<img src="assets/icons/png/user_b.png" alt="个人中心" class="icon-small">
|
||
个人中心
|
||
</div>
|
||
<div class="dropdown-item admin-only" data-action="users">
|
||
<img src="assets/icons/png/users_b.png" alt="用户管理" class="icon-small">
|
||
用户管理
|
||
</div>
|
||
<div class="dropdown-divider"></div>
|
||
<div class="dropdown-item" data-action="logout">
|
||
<img src="assets/icons/png/logout_b.png" alt="退出登录" class="icon-small">
|
||
退出登录
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
addEventListeners() {
|
||
const userDropdown = this.shadowRoot.querySelector('.user-dropdown');
|
||
const userDropdownToggle = this.shadowRoot.querySelector('.user-dropdown-toggle');
|
||
const userIconWrapper = this.shadowRoot.querySelector('.user-icon-wrapper');
|
||
|
||
// 点击切换下拉菜单
|
||
userDropdownToggle.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
userDropdown.classList.toggle('active');
|
||
});
|
||
|
||
// 点击其他地方关闭下拉菜单
|
||
document.addEventListener('click', () => {
|
||
userDropdown.classList.remove('active');
|
||
});
|
||
|
||
// 处理下拉菜单项点击
|
||
const dropdownItems = this.shadowRoot.querySelectorAll('.dropdown-item');
|
||
dropdownItems.forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const action = item.getAttribute('data-action');
|
||
this.handleDropdownAction(action);
|
||
userDropdown.classList.remove('active');
|
||
});
|
||
});
|
||
|
||
// 处理tooltip的定位
|
||
userIconWrapper.addEventListener('mouseenter', () => {
|
||
const tooltip = this.shadowRoot.querySelector('.tooltip');
|
||
const rect = userIconWrapper.getBoundingClientRect();
|
||
const tooltipRect = tooltip.getBoundingClientRect();
|
||
|
||
// 检查右边界
|
||
if (rect.left + tooltipRect.width > window.innerWidth) {
|
||
// 计算tooltip应该距离右边界的位置
|
||
const rightEdge = window.innerWidth;
|
||
const tooltipWidth = tooltipRect.width;
|
||
tooltip.style.position = 'fixed';
|
||
tooltip.style.left = 'auto';
|
||
tooltip.style.right = '0';
|
||
tooltip.style.transform = 'none';
|
||
tooltip.style.margin = '0';
|
||
tooltip.style.top = `${rect.bottom + 8}px`;
|
||
} else {
|
||
tooltip.style.position = 'absolute';
|
||
tooltip.style.left = '50%';
|
||
tooltip.style.right = 'auto';
|
||
tooltip.style.transform = 'translateX(-50%)';
|
||
tooltip.style.margin = '8px 0 0 0';
|
||
tooltip.style.top = '100%';
|
||
}
|
||
});
|
||
}
|
||
|
||
handleDropdownAction(action) {
|
||
switch(action) {
|
||
case 'profile':
|
||
this.dispatchEvent(new CustomEvent('menu-action', {
|
||
detail: { action: 'profile' },
|
||
bubbles: true,
|
||
composed: true
|
||
}));
|
||
break;
|
||
case 'users':
|
||
this.dispatchEvent(new CustomEvent('menu-action', {
|
||
detail: { action: 'users' },
|
||
bubbles: true,
|
||
composed: true
|
||
}));
|
||
break;
|
||
case 'logout':
|
||
this.logout();
|
||
break;
|
||
}
|
||
}
|
||
|
||
async checkLoginStatus() {
|
||
try {
|
||
const response = await fetch('/api/check-auth', {
|
||
credentials: 'include'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.updateUserInfo(result.user);
|
||
} else {
|
||
document.getElementById('authContainer').style.display = 'block';
|
||
document.getElementById('mainContainer').style.display = 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户信息失败:', error);
|
||
document.getElementById('authContainer').style.display = 'block';
|
||
document.getElementById('mainContainer').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
updateUserInfo(userInfo) {
|
||
// 设置用户名
|
||
this.shadowRoot.getElementById('userName').textContent = userInfo.username;
|
||
|
||
// 设置用户图标和tooltip信息
|
||
const userIcon = this.shadowRoot.getElementById('userIcon');
|
||
const userTooltip = this.shadowRoot.getElementById('userTooltip');
|
||
|
||
// 使用用户自定义图标
|
||
if (userInfo.icon && userInfo.icon.trim() !== '') {
|
||
const base64Data = userInfo.icon.trim();
|
||
|
||
// 检查Base64数据是否有效且看起来像图片数据
|
||
if (base64Data.length > 100 &&
|
||
/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data)) {
|
||
|
||
// 根据Base64数据开头判断图片格式
|
||
let mimeType = 'image/jpeg'; // 默认
|
||
if (base64Data.startsWith('iVBORw0KGgo')) {
|
||
mimeType = 'image/png';
|
||
} else if (base64Data.startsWith('R0lGODlh')) {
|
||
mimeType = 'image/gif';
|
||
} else if (base64Data.startsWith('UklGR')) {
|
||
mimeType = 'image/webp';
|
||
}
|
||
|
||
userIcon.src = `data:${mimeType};base64,${base64Data}`;
|
||
} else {
|
||
console.warn('无效的图片Base64数据格式,使用默认头像');
|
||
userIcon.src = 'assets/icons/png/user_b.png';
|
||
}
|
||
} else {
|
||
// 如果没有自定义图标,使用默认用户图标
|
||
userIcon.src = 'assets/icons/png/user_b.png';
|
||
}
|
||
userIcon.alt = userInfo.username;
|
||
|
||
// 添加图片加载错误处理
|
||
userIcon.onerror = () => {
|
||
console.warn('用户头像加载失败,使用默认头像');
|
||
console.warn('尝试加载的URL:', userIcon.src);
|
||
userIcon.src = 'assets/icons/png/user_b.png';
|
||
};
|
||
|
||
// 构建HTML内容
|
||
const tooltipContent = `
|
||
<div class="info-row">
|
||
<span class="label">用户名</span>
|
||
<span class="value">${userInfo.username || '未设置'}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">真实姓名</span>
|
||
<span class="value">${userInfo.full_name || '未设置'}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">权限级别</span>
|
||
<span class="value">${this.getAccessLevelName(userInfo.access_level)}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">所属部门</span>
|
||
<span class="value">${userInfo.department || '未设置'}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">职位</span>
|
||
<span class="value">${userInfo.position || '未设置'}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">电子邮箱</span>
|
||
<span class="value">${userInfo.email || '未设置'}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">联系电话</span>
|
||
<span class="value">${userInfo.phone || '未设置'}</span>
|
||
</div>
|
||
`;
|
||
|
||
userTooltip.innerHTML = tooltipContent;
|
||
|
||
// 控制用户管理选项的显示
|
||
const userManagementItem = this.shadowRoot.querySelector('.admin-only');
|
||
if (userManagementItem) {
|
||
userManagementItem.style.display = parseInt(userInfo.access_level) >= 3 ? 'flex' : 'none';
|
||
}
|
||
}
|
||
|
||
getAccessLevelName(accessLevel) {
|
||
const level = parseInt(accessLevel);
|
||
switch(level) {
|
||
case 1: return '普通用户';
|
||
case 2: return '开发者';
|
||
case 3: return '组长';
|
||
case 4: return '管理员';
|
||
default: return '访客';
|
||
}
|
||
}
|
||
|
||
async logout() {
|
||
try {
|
||
const response = await fetch('/api/logout', {
|
||
method: 'POST',
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 获取认证容器和主容器
|
||
const authContainer = document.getElementById('authContainer');
|
||
const mainContainer = document.getElementById('mainContainer');
|
||
|
||
// 先隐藏主容器
|
||
mainContainer.classList.remove('visible');
|
||
// 等待过渡效果完成
|
||
setTimeout(() => {
|
||
location.reload();
|
||
|
||
mainContainer.style.display = 'none';
|
||
// 显示认证容器
|
||
authContainer.style.display = 'block';
|
||
// 等待一帧以确保display:block生效
|
||
requestAnimationFrame(() => {
|
||
authContainer.classList.add('visible');
|
||
// 重置认证组件
|
||
const authComponent = document.querySelector('auth-component');
|
||
if (authComponent) {
|
||
authComponent.reset();
|
||
}
|
||
});
|
||
}, 300);
|
||
} else {
|
||
console.error('登出失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('登出错误:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
customElements.define('user-info', UserInfo);
|