680 lines
18 KiB
HTML
Executable File
680 lines
18 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
{% include 'admin_nav.html' %}
|
|
|
|
<div class="admin-container">
|
|
<div class="admin-content">
|
|
<div class="admin-card">
|
|
<div class="card-header">
|
|
<div class="header-left">
|
|
<i class="fas fa-users"></i>
|
|
<h3>用户管理</h3>
|
|
</div>
|
|
<div class="filter-sort">
|
|
<div class="sort-box">
|
|
<i class="fas fa-sort"></i>
|
|
<select class="sort-select" onchange="sortUsers(this)">
|
|
<option value="created_desc">注册时间 ↓</option>
|
|
<option value="created_asc">注册时间 ↑</option>
|
|
<option value="login_desc">最近登录 ↓</option>
|
|
<option value="login_asc">最近登录 ↑</option>
|
|
<option value="invite_desc">邀请人数 ↓</option>
|
|
<option value="invite_asc">邀请人数 ↑</option>
|
|
</select>
|
|
</div>
|
|
<div class="search-box">
|
|
<input type="text"
|
|
class="search-input"
|
|
placeholder="搜索用户..."
|
|
onkeyup="filterUsers(this)">
|
|
<i class="fas fa-search"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="users-table-container">
|
|
<table class="users-table">
|
|
<thead>
|
|
<tr>
|
|
<th>头像</th>
|
|
<th>昵称</th>
|
|
<th>邮箱</th>
|
|
<th>华为ID</th>
|
|
<th>邀请人数</th>
|
|
<th>被邀请者</th>
|
|
<th>注册时间</th>
|
|
<th>最近登录</th>
|
|
<th>登录次数</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in users %}
|
|
<tr>
|
|
<td>
|
|
<img src="{{ user.avatar or '/static/images/default-avatar.png' }}"
|
|
alt="{{ user.name }}"
|
|
class="user-avatar">
|
|
</td>
|
|
<td title="{{ user.name }}">{{ user.name }}</td>
|
|
<td class="user-email">{{ user.email or '未设置' }}</td>
|
|
<td class="user-id">{{ user.huawei_id }}</td>
|
|
<td>
|
|
<div class="invite-count">
|
|
<span>{{ user.invite_count }}</span>
|
|
{% if user.invite_count > 0 %}
|
|
<button class="btn-view" onclick="viewInvitees('{{ user.id }}')">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if user.inviter_name %}
|
|
<span class="inviter-tag">
|
|
<i class="fas fa-user-plus"></i>
|
|
{{ user.inviter_name }}
|
|
</span>
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="time-info">
|
|
<span class="time-label">注册时间</span>
|
|
<span class="time-value">{{ user.created_at }}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="time-info">
|
|
<span class="time-label">最近登录</span>
|
|
<span class="time-value">{{ user.last_login or '未登录' }}</span>
|
|
</div>
|
|
</td>
|
|
<td>{{ user.login_count }}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn-delete" onclick="deleteUser('{{ user.id }}', '{{ user.name }}')" title="删除">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 被邀请用户列表弹窗 -->
|
|
<div id="inviteesModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>邀请的用户</h3>
|
|
<button class="close-btn" onclick="closeInviteesModal()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div id="inviteesList" class="invitees-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast 容器 -->
|
|
<div id="toast" class="toast"></div>
|
|
|
|
<style>
|
|
/* 容器样式优化 */
|
|
.admin-container {
|
|
padding: 0; /* 移除容器的内边距 */
|
|
}
|
|
|
|
.admin-content {
|
|
padding: 0; /* 移除内容区的内边距 */
|
|
max-width: none; /* 移除最大宽度限制 */
|
|
}
|
|
|
|
/* 卡片样式优化 */
|
|
.admin-card {
|
|
background: white;
|
|
border-radius: 0; /* 移除圆角 */
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
margin: 0; /* 移除外边距 */
|
|
}
|
|
|
|
/* 表头样式优化 */
|
|
.card-header {
|
|
padding: 10px 12px; /* 减小表头内边距 */
|
|
border-bottom: 1px solid #eee;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: white;
|
|
position: sticky; /* 使表头固定 */
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px; /* 减小间距 */
|
|
}
|
|
|
|
.header-left i {
|
|
font-size: 16px; /* 减小图标尺寸 */
|
|
color: #1890ff;
|
|
}
|
|
|
|
.header-left h3 {
|
|
margin: 0;
|
|
font-size: 16px; /* 减小标题字号 */
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* 表格容器样式 */
|
|
.users-table-container {
|
|
padding: 0; /* 移除内边距 */
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.users-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 0;
|
|
border: none; /* 移除表格边框 */
|
|
}
|
|
|
|
.users-table th,
|
|
.users-table td {
|
|
padding: 8px 12px; /* 减小单元格内边距 */
|
|
text-align: left;
|
|
border-bottom: 1px solid #eee;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.users-table th {
|
|
background: #fafafa;
|
|
font-weight: 600;
|
|
color: #666;
|
|
font-size: 12px; /* 减小表头字号 */
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.users-table td {
|
|
font-size: 13px; /* 减小单元格字号 */
|
|
color: #333;
|
|
}
|
|
|
|
/* 昵称列宽度限制 */
|
|
.users-table td:nth-child(2) { /* 昵称是第二列 */
|
|
max-width: 120px; /* 限制最大宽度 */
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* 鼠标悬停时显示完整昵称 */
|
|
.users-table td:nth-child(2):hover {
|
|
position: relative;
|
|
}
|
|
|
|
.users-table td:nth-child(2):hover::after {
|
|
content: attr(title);
|
|
position: absolute;
|
|
left: 0;
|
|
top: 100%;
|
|
background: #fff;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
z-index: 1000;
|
|
white-space: normal;
|
|
max-width: 200px;
|
|
word-break: break-all;
|
|
}
|
|
|
|
/* 暗色模式适配 */
|
|
[data-theme="dark"] .users-table td:nth-child(2):hover::after {
|
|
background: #333;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
/* 用户头像样式优化 */
|
|
.users-table td:first-child { /* 头像是第一列 */
|
|
width: 40px; /* 固定宽度 */
|
|
padding: 8px 4px; /* 减小内边距 */
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 32px; /* 减小头像尺寸 */
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 1px solid #fff; /* 减小边框 */
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
/* 邮箱和ID样式 */
|
|
.user-email,
|
|
.user-id {
|
|
color: #666;
|
|
font-size: 12px; /* 减小字号 */
|
|
}
|
|
|
|
/* 邀请数量样式 */
|
|
.invite-count {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px; /* 减小间距 */
|
|
}
|
|
|
|
.invite-count span {
|
|
font-weight: 500;
|
|
color: #1890ff;
|
|
}
|
|
|
|
.inviter-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 8px;
|
|
background: rgba(24, 144, 255, 0.1);
|
|
color: #1890ff;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.inviter-tag:hover {
|
|
background: rgba(24, 144, 255, 0.2);
|
|
}
|
|
|
|
.inviter-tag i {
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* 时间显示样式 */
|
|
.time-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px; /* 减小间距 */
|
|
}
|
|
|
|
.time-label {
|
|
font-size: 12px;
|
|
color: #999;
|
|
line-height: 1.2; /* 减小行高 */
|
|
}
|
|
|
|
.time-value {
|
|
font-size: 13px;
|
|
color: #333;
|
|
line-height: 1.2; /* 减小行高 */
|
|
}
|
|
|
|
/* 按钮样式优化 */
|
|
.btn-view,
|
|
.btn-delete {
|
|
padding: 4px; /* 减小按钮内边距 */
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
width: 24px; /* 减小按钮尺寸 */
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-view {
|
|
background: #1890ff;
|
|
color: white;
|
|
}
|
|
|
|
.btn-delete {
|
|
background: #ff4d4f;
|
|
color: white;
|
|
}
|
|
|
|
.btn-view:hover {
|
|
background: #40a9ff;
|
|
}
|
|
|
|
.btn-delete:hover {
|
|
background: #ff7875;
|
|
}
|
|
|
|
/* 暗色模式适配 */
|
|
[data-theme="dark"] .admin-card {
|
|
background: #242424;
|
|
box-shadow: none;
|
|
}
|
|
|
|
[data-theme="dark"] .card-header {
|
|
background: #242424;
|
|
border-color: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .users-table th {
|
|
background: #1f1f1f;
|
|
color: #999;
|
|
}
|
|
|
|
[data-theme="dark"] .users-table td {
|
|
color: #ccc;
|
|
}
|
|
|
|
[data-theme="dark"] .user-email,
|
|
[data-theme="dark"] .user-id {
|
|
color: #999;
|
|
}
|
|
|
|
[data-theme="dark"] .time-label {
|
|
color: #666;
|
|
}
|
|
|
|
[data-theme="dark"] .time-value {
|
|
color: #ccc;
|
|
}
|
|
|
|
.invitees-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.invitee-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.invitee-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.invitee-name {
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.invitee-email {
|
|
color: #666;
|
|
font-size: 12px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.invitee-date {
|
|
color: #999;
|
|
font-size: 12px;
|
|
}
|
|
|
|
[data-theme="dark"] .invitee-item {
|
|
border-color: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .invitee-email {
|
|
color: #999;
|
|
}
|
|
|
|
[data-theme="dark"] .invitee-date {
|
|
color: #666;
|
|
}
|
|
|
|
/* 搜索和排序区域样式 */
|
|
.filter-sort {
|
|
display: flex;
|
|
gap: 8px; /* 减小间距 */
|
|
align-items: center;
|
|
}
|
|
|
|
.sort-box {
|
|
position: relative;
|
|
}
|
|
|
|
.sort-box i {
|
|
position: absolute;
|
|
left: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #666;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.sort-select {
|
|
padding: 6px 10px 6px 30px; /* 减小内边距 */
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
font-size: 13px; /* 减小字号 */
|
|
color: #333;
|
|
background: white;
|
|
cursor: pointer;
|
|
appearance: none;
|
|
min-width: 140px; /* 减小最小宽度 */
|
|
}
|
|
|
|
.search-box {
|
|
position: relative;
|
|
flex: 1;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
padding: 6px 10px 6px 30px; /* 减小内边距 */
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
font-size: 13px; /* 减小字号 */
|
|
}
|
|
|
|
.search-box i {
|
|
position: absolute;
|
|
left: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #666;
|
|
}
|
|
|
|
/* 暗色模式适配 */
|
|
[data-theme="dark"] .sort-select,
|
|
[data-theme="dark"] .search-input {
|
|
background: #333;
|
|
border-color: #444;
|
|
color: #ccc;
|
|
}
|
|
|
|
[data-theme="dark"] .sort-box i,
|
|
[data-theme="dark"] .search-box i {
|
|
color: #999;
|
|
}
|
|
|
|
/* 修改滚动条样式 */
|
|
.users-table-container::-webkit-scrollbar {
|
|
height: 8px; /* 水平滚动条高度 */
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.users-table-container::-webkit-scrollbar-thumb {
|
|
background-color: #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.users-table-container::-webkit-scrollbar-thumb:hover {
|
|
background-color: #ccc;
|
|
}
|
|
|
|
[data-theme="dark"] .users-table-container::-webkit-scrollbar {
|
|
background-color: #1a1a1a;
|
|
}
|
|
|
|
[data-theme="dark"] .users-table-container::-webkit-scrollbar-thumb {
|
|
background-color: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .users-table-container::-webkit-scrollbar-thumb:hover {
|
|
background-color: #444;
|
|
}
|
|
|
|
/* Modal 默认样式 */
|
|
.modal {
|
|
display: none; /* 默认隐藏 */
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-content {
|
|
background: white;
|
|
border-radius: 8px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 80vh;
|
|
position: relative;
|
|
}
|
|
|
|
[data-theme="dark"] .modal-content {
|
|
background: #242424;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function showToast(message) {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.style.display = 'block';
|
|
|
|
setTimeout(() => {
|
|
toast.style.display = 'none';
|
|
}, 3000);
|
|
}
|
|
|
|
function viewInvitees(userId) {
|
|
// 先清空内容
|
|
const inviteesList = document.getElementById('inviteesList');
|
|
inviteesList.innerHTML = '';
|
|
|
|
fetch(`/admin/users/${userId}/invitees`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
inviteesList.innerHTML = data.invitees.map(invitee => `
|
|
<div class="invitee-item">
|
|
<img src="${invitee.avatar || '/static/images/default-avatar.png'}"
|
|
alt="${invitee.name}"
|
|
class="user-avatar">
|
|
<div class="invitee-info">
|
|
<div class="invitee-name">${invitee.name}</div>
|
|
<div class="invitee-email">${invitee.email || '未设置邮箱'}</div>
|
|
<div class="invitee-date">邀请时间:${invitee.invite_date}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// 显示 modal
|
|
document.getElementById('inviteesModal').style.display = 'flex';
|
|
} else {
|
|
showToast(data.error || '获取邀请用户列表失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Failed to get invitees:', error);
|
|
showToast('获取邀请用户列表失败');
|
|
});
|
|
}
|
|
|
|
function closeInviteesModal() {
|
|
const modal = document.getElementById('inviteesModal');
|
|
modal.style.display = 'none';
|
|
// 清空弹窗内容,防止下次打开时显示旧数据
|
|
document.getElementById('inviteesList').innerHTML = '';
|
|
}
|
|
|
|
function deleteUser(userId, userName) {
|
|
if (!confirm(`确定要删除用户"${userName}"吗?此操作不可恢复。`)) return;
|
|
|
|
fetch(`/admin/users/${userId}/delete`, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
showToast(data.error || '删除失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Delete failed:', error);
|
|
showToast('删除失败,请稍后重试');
|
|
});
|
|
}
|
|
|
|
function sortUsers(select) {
|
|
const [field, order] = select.value.split('_');
|
|
const tbody = document.querySelector('.users-table tbody');
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
|
|
rows.sort((a, b) => {
|
|
let aValue, bValue;
|
|
|
|
switch(field) {
|
|
case 'created':
|
|
aValue = new Date(a.cells[6].textContent);
|
|
bValue = new Date(b.cells[6].textContent);
|
|
break;
|
|
case 'login':
|
|
aValue = a.cells[7].textContent === '未登录' ? new Date(0) : new Date(a.cells[7].textContent);
|
|
bValue = b.cells[7].textContent === '未登录' ? new Date(0) : new Date(b.cells[7].textContent);
|
|
break;
|
|
case 'invite':
|
|
aValue = parseInt(a.cells[4].textContent);
|
|
bValue = parseInt(b.cells[4].textContent);
|
|
break;
|
|
}
|
|
|
|
return order === 'asc' ? aValue - bValue : bValue - aValue;
|
|
});
|
|
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
}
|
|
|
|
function filterUsers(input) {
|
|
const filter = input.value.toLowerCase();
|
|
const rows = document.querySelectorAll('.users-table tbody tr');
|
|
|
|
rows.forEach(row => {
|
|
const name = row.cells[1].textContent.toLowerCase();
|
|
const email = row.cells[2].textContent.toLowerCase();
|
|
const huaweiId = row.cells[3].textContent.toLowerCase();
|
|
|
|
const matches = name.includes(filter) ||
|
|
email.includes(filter) ||
|
|
huaweiId.includes(filter);
|
|
|
|
row.style.display = matches ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
// 点击遮罩层关闭弹窗时也清空内容
|
|
document.getElementById('inviteesModal').addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeInviteesModal();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% endblock %} |