Files
ns2.0/templates/admin_users.html

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 %}