XNSim/XNSimHtml/components/profile-center.js

1035 lines
37 KiB
JavaScript
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.

class ProfileCenter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.userInfo = null;
this.isEditing = false;
}
connectedCallback() {
this.render();
this.loadUserInfo();
this.addEventListeners();
}
async loadUserInfo() {
try {
const response = await fetch('/api/check-auth', {
credentials: 'include'
});
const result = await response.json();
if (result.success) {
this.userInfo = result.user;
this.updateDisplay();
} else {
this.showError('获取用户信息失败');
}
} catch (error) {
console.error('获取用户信息失败:', error);
this.showError('获取用户信息失败');
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.profile-container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow: hidden;
}
.profile-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px 24px;
text-align: center;
position: relative;
}
.avatar-section {
margin-bottom: 15px;
position: relative;
display: inline-block;
}
.avatar-wrapper {
position: relative;
display: inline-block;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.3);
object-fit: cover;
margin: 0 auto 12px;
display: block;
background: rgba(255,255,255,0.1);
}
.avatar-edit-btn {
position: absolute;
bottom: 5px;
right: 5px;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(255,255,255,0.95);
border: 2px solid #667eea;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 5;
}
.avatar-edit-btn:hover {
background: white;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.avatar-edit-btn img {
width: 14px;
height: 14px;
opacity: 0.8;
}
.avatar-edit-btn:hover img {
opacity: 1;
}
.file-input {
display: none;
}
.upload-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
z-index: 10;
}
.username {
font-size: 24px;
font-weight: 600;
margin: 0 0 6px 0;
}
.user-role {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
.profile-content {
padding: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
display: flex;
align-items: center;
gap: 8px;
}
.section-title img {
width: 18px;
height: 18px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.info-card {
background: #f8f9fa;
border-radius: 10px;
padding: 16px;
border: 1px solid #e9ecef;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #e9ecef;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: #6c757d;
min-width: 90px;
}
.info-value {
color: #333;
text-align: right;
flex: 1;
margin-left: 16px;
}
.info-value.empty {
color: #adb5bd;
font-style: italic;
}
.edit-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 14px;
background: white;
}
.edit-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.action-buttons {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 24px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
}
.btn img {
width: 16px;
height: 16px;
}
.permission-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.permission-admin {
background: #dc3545;
color: white;
}
.permission-leader {
background: #fd7e14;
color: white;
}
.permission-developer {
background: #17a2b8;
color: white;
}
.permission-user {
background: #28a745;
color: white;
}
.permission-guest {
background: #6c757d;
color: white;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
text-align: center;
}
.success-message {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
text-align: center;
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: white;
border-radius: 12px;
padding: 24px;
width: 90%;
max-width: 400px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
transform: scale(0.9);
transition: transform 0.3s ease;
}
.modal-overlay.active .modal {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #6c757d;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
color: #333;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
}
.btn-small {
padding: 8px 16px;
font-size: 14px;
}
.modal-message {
margin-bottom: 16px;
padding: 12px;
border-radius: 6px;
font-size: 14px;
text-align: center;
}
.modal-message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.modal-message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
@media (max-width: 768px) {
:host {
padding: 10px;
}
.profile-container {
margin: 0;
}
.info-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
}
</style>
<div class="profile-container">
<div class="profile-header">
<div class="avatar-section">
<div class="avatar-wrapper">
<img id="userAvatar" class="avatar" src="assets/icons/png/user.png" alt="用户头像">
<div id="avatarEditBtn" class="avatar-edit-btn">
<img src="assets/icons/png/sliders_b.png" alt="编辑头像">
</div>
</div>
<input id="avatarFileInput" type="file" class="file-input" accept="image/*">
<div id="uploadLoading" class="upload-loading" style="display: none;">上传中...</div>
<h1 id="userName" class="username">加载中...</h1>
<p id="userRole" class="user-role">加载中...</p>
</div>
</div>
<div class="profile-content">
<div id="errorMessage" class="error-message" style="display: none;"></div>
<div id="successMessage" class="success-message" style="display: none;"></div>
<div class="section-title">
<img src="assets/icons/png/user_b.png" alt="基本信息">
基本信息
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-item">
<span class="info-label">用户名</span>
<span id="username" class="info-value">-</span>
</div>
<div class="info-item">
<span class="info-label">真实姓名</span>
<span id="fullName" class="info-value">-</span>
</div>
<div class="info-item">
<span class="info-label">权限级别</span>
<span id="accessLevel" class="info-value">-</span>
</div>
</div>
<div class="info-card">
<div class="info-item">
<span class="info-label">所属部门</span>
<span id="department" class="info-value">-</span>
</div>
<div class="info-item">
<span class="info-label">职位</span>
<span id="position" class="info-value">-</span>
</div>
<div class="info-item">
<span class="info-label">用户ID</span>
<span id="userId" class="info-value">-</span>
</div>
</div>
</div>
<div class="section-title">
<img src="assets/icons/png/mail.png" alt="联系方式">
联系方式
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-item">
<span class="info-label">电子邮箱</span>
<span id="email" class="info-value">-</span>
</div>
<div class="info-item">
<span class="info-label">联系电话</span>
<span id="phone" class="info-value">-</span>
</div>
</div>
</div>
<div class="action-buttons">
<button id="editBtn" class="btn btn-primary">
<img src="assets/icons/png/sliders.png" alt="编辑">
编辑信息
</button>
<button id="changePasswordBtn" class="btn btn-primary">
<img src="assets/icons/png/password.png" alt="修改密码">
修改密码
</button>
<button id="saveBtn" class="btn btn-primary" style="display: none;">
<img src="assets/icons/png/check-square.png" alt="保存">
保存更改
</button>
<button id="cancelBtn" class="btn btn-secondary" style="display: none;">
<img src="assets/icons/png/cancel.png" alt="取消">
取消编辑
</button>
</div>
</div>
</div>
<!-- 修改密码模态框 -->
<div id="changePasswordModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">修改密码</h3>
<button class="modal-close" id="closePasswordModal">&times;</button>
</div>
<div id="passwordModalMessage" class="modal-message" style="display: none;"></div>
<form id="changePasswordForm">
<div class="form-group">
<label class="form-label" for="oldPassword">当前密码</label>
<input type="password" id="oldPassword" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label" for="newPassword">新密码</label>
<input type="password" id="newPassword" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label" for="confirmPassword">确认新密码</label>
<input type="password" id="confirmPassword" class="form-input" required>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary btn-small" id="cancelPasswordChange">取消</button>
<button type="submit" class="btn btn-primary btn-small">确认修改</button>
</div>
</form>
</div>
</div>
`;
}
updateDisplay() {
if (!this.userInfo) return;
// 更新头像
const avatar = this.shadowRoot.getElementById('userAvatar');
if (this.userInfo.icon && this.userInfo.icon.trim() !== '') {
const base64Data = this.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';
}
avatar.src = `data:${mimeType};base64,${base64Data}`;
} else {
avatar.src = 'assets/icons/png/user.png';
}
} else {
avatar.src = 'assets/icons/png/user.png';
}
// 更新基本信息
this.shadowRoot.getElementById('userName').textContent = this.userInfo.username || '未设置';
this.shadowRoot.getElementById('userRole').textContent = this.getAccessLevelName(this.userInfo.access_level);
// 更新详细信息
this.updateInfoField('username', this.userInfo.username);
this.updateInfoField('fullName', this.userInfo.full_name);
this.updateInfoField('accessLevel', this.getAccessLevelName(this.userInfo.access_level), true);
this.updateInfoField('department', this.userInfo.department);
this.updateInfoField('position', this.userInfo.position);
this.updateInfoField('userId', this.userInfo.id);
this.updateInfoField('email', this.userInfo.email);
this.updateInfoField('phone', this.userInfo.phone);
}
updateInfoField(fieldId, value, isBadge = false) {
const element = this.shadowRoot.getElementById(fieldId);
if (element) {
if (isBadge) {
element.innerHTML = `<span class="permission-badge permission-${this.getPermissionClass(this.userInfo.access_level)}">${value}</span>`;
} else {
if (value && value !== '') {
element.textContent = value;
element.classList.remove('empty');
} else {
element.textContent = '未设置';
element.classList.add('empty');
}
}
}
}
getAccessLevelName(accessLevel) {
const level = parseInt(accessLevel);
switch(level) {
case 1: return '普通用户';
case 2: return '开发者';
case 3: return '组长';
case 4: return '管理员';
default: return '访客';
}
}
getPermissionClass(accessLevel) {
const level = parseInt(accessLevel);
switch(level) {
case 1: return 'user';
case 2: return 'developer';
case 3: return 'leader';
case 4: return 'admin';
default: return 'guest';
}
}
addEventListeners() {
const editBtn = this.shadowRoot.getElementById('editBtn');
const saveBtn = this.shadowRoot.getElementById('saveBtn');
const cancelBtn = this.shadowRoot.getElementById('cancelBtn');
const changePasswordBtn = this.shadowRoot.getElementById('changePasswordBtn');
const avatarEditBtn = this.shadowRoot.getElementById('avatarEditBtn');
const avatarFileInput = this.shadowRoot.getElementById('avatarFileInput');
editBtn.addEventListener('click', () => this.startEditing());
saveBtn.addEventListener('click', () => this.saveChanges());
cancelBtn.addEventListener('click', () => this.cancelEditing());
changePasswordBtn.addEventListener('click', () => this.showChangePasswordModal());
// 头像编辑按钮点击事件
avatarEditBtn.addEventListener('click', () => {
avatarFileInput.click();
});
// 文件选择事件
avatarFileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
this.handleAvatarUpload(file);
}
});
}
startEditing() {
this.isEditing = true;
this.shadowRoot.getElementById('editBtn').style.display = 'none';
this.shadowRoot.getElementById('saveBtn').style.display = 'flex';
this.shadowRoot.getElementById('cancelBtn').style.display = 'flex';
// 将可编辑字段转换为输入框
this.convertToInput('fullName', this.userInfo.full_name || '');
this.convertToInput('department', this.userInfo.department || '');
this.convertToInput('position', this.userInfo.position || '');
this.convertToInput('email', this.userInfo.email || '');
this.convertToInput('phone', this.userInfo.phone || '');
}
convertToInput(fieldId, value) {
const element = this.shadowRoot.getElementById(fieldId);
if (element) {
const input = document.createElement('input');
input.type = 'text';
input.className = 'edit-input';
input.value = value;
input.id = `${fieldId}Input`;
element.innerHTML = '';
element.appendChild(input);
}
}
async saveChanges() {
const updatedInfo = {
full_name: this.shadowRoot.getElementById('fullNameInput')?.value || '',
department: this.shadowRoot.getElementById('departmentInput')?.value || '',
position: this.shadowRoot.getElementById('positionInput')?.value || '',
email: this.shadowRoot.getElementById('emailInput')?.value || '',
phone: this.shadowRoot.getElementById('phoneInput')?.value || ''
};
try {
const response = await fetch('/api/update-user-info', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: this.userInfo.id,
userInfo: updatedInfo
}),
credentials: 'include'
});
const result = await response.json();
if (result.success) {
// 更新本地用户信息
this.userInfo = { ...this.userInfo, ...updatedInfo };
this.showSuccess('用户信息更新成功');
this.cancelEditing();
this.updateDisplay();
} else {
this.showError('更新失败: ' + result.message);
}
} catch (error) {
console.error('更新用户信息失败:', error);
this.showError('更新用户信息失败');
}
}
cancelEditing() {
this.isEditing = false;
this.shadowRoot.getElementById('editBtn').style.display = 'flex';
this.shadowRoot.getElementById('saveBtn').style.display = 'none';
this.shadowRoot.getElementById('cancelBtn').style.display = 'none';
// 恢复显示模式
this.updateDisplay();
}
showError(message) {
const errorElement = this.shadowRoot.getElementById('errorMessage');
errorElement.textContent = message;
errorElement.style.display = 'block';
setTimeout(() => {
errorElement.style.display = 'none';
}, 5000);
}
showSuccess(message) {
const successElement = this.shadowRoot.getElementById('successMessage');
successElement.textContent = message;
successElement.style.display = 'block';
setTimeout(() => {
successElement.style.display = 'none';
}, 3000);
}
async handleAvatarUpload(file) {
// 验证文件类型
if (!file.type.startsWith('image/')) {
this.showError('请选择图片文件');
return;
}
// 验证文件大小限制为500KB
if (file.size > 500 * 1024) {
this.showError('图片大小不能超过500KB');
return;
}
try {
// 显示上传中状态
const loadingElement = this.shadowRoot.getElementById('uploadLoading');
loadingElement.style.display = 'block';
// 压缩图片并转换为Base64
const base64Data = await this.compressAndConvertToBase64(file);
// 提取纯Base64数据去掉data URL前缀
const pureBase64 = base64Data.replace(/^data:image\/[a-z]+;base64,/, '');
// 调用后端API更新头像
const response = await fetch('/api/update-user-icon', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: this.userInfo.id,
iconBase64: pureBase64
}),
credentials: 'include'
});
const result = await response.json();
if (result.success) {
// 更新本地头像显示
const avatar = this.shadowRoot.getElementById('userAvatar');
avatar.src = base64Data;
// 更新用户信息中的头像
this.userInfo.icon = base64Data;
this.showSuccess('头像更新成功');
} else {
this.showError('头像更新失败: ' + (result.error || result.message));
}
} catch (error) {
console.error('头像上传失败:', error);
this.showError('头像上传失败');
} finally {
// 隐藏上传中状态
const loadingElement = this.shadowRoot.getElementById('uploadLoading');
loadingElement.style.display = 'none';
// 清空文件输入框
this.shadowRoot.getElementById('avatarFileInput').value = '';
}
}
compressAndConvertToBase64(file) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// 设置画布尺寸限制最大尺寸为200x200
const maxSize = 200;
let { width, height } = img;
if (width > height) {
if (width > maxSize) {
height = (height * maxSize) / width;
width = maxSize;
}
} else {
if (height > maxSize) {
width = (width * maxSize) / height;
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
// 清空画布并设置透明背景
ctx.clearRect(0, 0, width, height);
// 绘制图片到画布
ctx.drawImage(img, 0, 0, width, height);
// 根据原图片格式选择合适的输出格式
let mimeType = 'image/png'; // 默认使用PNG保持透明背景
let quality = 0.9;
// 如果原图是JPEG格式则输出JPEG
if (file.type === 'image/jpeg' || file.type === 'image/jpg') {
mimeType = 'image/jpeg';
quality = 0.8;
}
// 转换为Base64
const base64 = canvas.toDataURL(mimeType, quality);
resolve(base64);
};
img.onerror = () => {
reject(new Error('图片加载失败'));
};
// 从文件创建URL
const url = URL.createObjectURL(file);
img.src = url;
});
}
showChangePasswordModal() {
const modal = this.shadowRoot.getElementById('changePasswordModal');
modal.classList.add('active');
// 添加模态框事件监听器
this.addPasswordModalListeners();
}
addPasswordModalListeners() {
const modal = this.shadowRoot.getElementById('changePasswordModal');
const closeBtn = this.shadowRoot.getElementById('closePasswordModal');
const cancelBtn = this.shadowRoot.getElementById('cancelPasswordChange');
const form = this.shadowRoot.getElementById('changePasswordForm');
// 关闭按钮事件
closeBtn.addEventListener('click', () => this.hideChangePasswordModal());
// 取消按钮事件
cancelBtn.addEventListener('click', () => this.hideChangePasswordModal());
// 点击模态框背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.hideChangePasswordModal();
}
});
// 表单提交事件
form.addEventListener('submit', (e) => {
e.preventDefault();
this.handlePasswordChange();
});
}
hideChangePasswordModal() {
const modal = this.shadowRoot.getElementById('changePasswordModal');
modal.classList.remove('active');
// 清空表单
const form = this.shadowRoot.getElementById('changePasswordForm');
form.reset();
// 隐藏消息
const messageElement = this.shadowRoot.getElementById('passwordModalMessage');
messageElement.style.display = 'none';
}
async handlePasswordChange() {
const oldPassword = this.shadowRoot.getElementById('oldPassword').value;
const newPassword = this.shadowRoot.getElementById('newPassword').value;
const confirmPassword = this.shadowRoot.getElementById('confirmPassword').value;
// 验证输入
if (!oldPassword || !newPassword || !confirmPassword) {
this.showPasswordModalMessage('请填写所有密码字段', 'error');
return;
}
if (newPassword !== confirmPassword) {
this.showPasswordModalMessage('新密码和确认密码不匹配', 'error');
return;
}
if (newPassword.length < 6) {
this.showPasswordModalMessage('新密码长度至少6位', 'error');
return;
}
try {
const response = await fetch('/api/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: this.userInfo.id,
oldPassword: oldPassword,
newPassword: newPassword
}),
credentials: 'include'
});
const result = await response.json();
if (result.success) {
this.showPasswordModalMessage('密码修改成功', 'success');
setTimeout(() => {
this.hideChangePasswordModal();
}, 1500);
} else {
this.showPasswordModalMessage('密码修改失败: ' + result.message, 'error');
}
} catch (error) {
console.error('修改密码失败:', error);
this.showPasswordModalMessage('修改密码失败', 'error');
}
}
showPasswordModalMessage(message, type) {
const messageElement = this.shadowRoot.getElementById('passwordModalMessage');
messageElement.textContent = message;
messageElement.className = `modal-message ${type}`;
messageElement.style.display = 'block';
// 如果是成功消息3秒后自动隐藏
if (type === 'success') {
setTimeout(() => {
messageElement.style.display = 'none';
}, 3000);
}
}
}
customElements.define('profile-center', ProfileCenter);