363 lines
16 KiB
HTML
Executable File
363 lines
16 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-user-plus"></i>
|
||
<h3>添加管理员</h3>
|
||
</div>
|
||
</div>
|
||
<form onsubmit="submitAddAdminForm(event, this)" class="admin-form">
|
||
<div class="form-group">
|
||
<label for="username">用户名</label>
|
||
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="password">密码</label>
|
||
<div class="password-input-wrapper">
|
||
<input type="password" id="password" name="password" placeholder="请输入密码" required>
|
||
<button type="button" class="toggle-password" onclick="togglePassword('password')">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="confirm_password">确认密码</label>
|
||
<div class="password-input-wrapper">
|
||
<input type="password" id="confirm_password" name="confirm_password" placeholder="请再次输入密码" required>
|
||
<button type="button" class="toggle-password" onclick="togglePassword('confirm_password')">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary">
|
||
<i class="fas fa-plus"></i> 添加管理员
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="admin-card full-width">
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<i class="fas fa-users"></i>
|
||
<h3>管理员列表</h3>
|
||
</div>
|
||
<form class="search-form header-search" method="GET">
|
||
<div class="search-wrapper">
|
||
<i class="fas fa-search"></i>
|
||
<input type="text" name="search" placeholder="搜索管理员..." value="{{ request.args.get('search', '') }}">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>用户名</th>
|
||
<th>角色</th>
|
||
<th>创建时间</th>
|
||
<th>最后登录</th>
|
||
<th>登录次数</th>
|
||
<th>最后操作</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for admin in admins %}
|
||
<tr>
|
||
<td>{{ admin.username }}</td>
|
||
<td>{{ '超级管理员' if admin.is_superadmin else '管理员' }}</td>
|
||
<td>{{ admin.created_at }}</td>
|
||
<td>{{ (admin.last_login|default('从未登录', true))|datetime_format }}</td>
|
||
<td>{{ admin.login_count }}</td>
|
||
<td>{{ admin.last_action or '无' }}</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
{% if not admin.is_superadmin %}
|
||
<a href="#" class="btn-view" onclick="showLogs('{{ admin.id }}'); return false;">
|
||
<i class="fas fa-history"></i>
|
||
</a>
|
||
<a href="{{ url_for('reset_admin_password', admin_id=admin.id) }}"
|
||
class="btn-edit"
|
||
onclick="return confirm('确定要重置该管理员的密码吗?')">
|
||
<i class="fas fa-key"></i>
|
||
</a>
|
||
<a href="#" class="btn-delete" onclick="deleteAdmin('{{ admin.id }}'); return false;">
|
||
<i class="fas fa-trash"></i>
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<!-- 管理员日志详情 -->
|
||
<tr id="logs-{{ admin.id }}" style="display: none;">
|
||
<td colspan="8">
|
||
<div class="admin-logs">
|
||
<h4>最近操作记录</h4>
|
||
<table class="logs-table">
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>操作</th>
|
||
<th>IP地址</th>
|
||
<th>浏览器信息</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for log in admin_logs[admin.id] %}
|
||
<tr>
|
||
<td>{{ log.created_at|datetime_format }}</td>
|
||
<td>{{ log.action }}</td>
|
||
<td>{{ log.ip_address }}</td>
|
||
<td>
|
||
{% if 'Mobile' in log.user_agent %}
|
||
{% if 'Android' in log.user_agent %}
|
||
{% if 'MI' in log.user_agent or 'XiaoMi' in log.user_agent or 'Redmi' in log.user_agent %}
|
||
小米{{ log.user_agent.split('MI ')[1].split(' ')[0] if 'MI ' in log.user_agent else log.user_agent.split('Redmi ')[1].split(' ')[0] if 'Redmi ' in log.user_agent else '' }}
|
||
{% elif 'HUAWEI' in log.user_agent %}
|
||
华为{{ log.user_agent.split('HUAWEI ')[1].split(' ')[0] if 'HUAWEI ' in log.user_agent else '' }}
|
||
{% elif 'HONOR' in log.user_agent %}
|
||
荣耀{{ log.user_agent.split('HONOR ')[1].split(' ')[0] if 'HONOR ' in log.user_agent else '' }}
|
||
{% elif 'OPPO' in log.user_agent %}
|
||
OPPO{{ log.user_agent.split('OPPO ')[1].split(' ')[0] if 'OPPO ' in log.user_agent else '' }}
|
||
{% elif 'vivo' in log.user_agent %}
|
||
vivo{{ log.user_agent.split('vivo ')[1].split(' ')[0] if 'vivo ' in log.user_agent else '' }}
|
||
{% elif 'SAMSUNG' in log.user_agent %}
|
||
三星{{ log.user_agent.split('SAMSUNG ')[1].split(' ')[0] if 'SAMSUNG ' in log.user_agent else '' }}
|
||
{% elif 'Build/' in log.user_agent %}
|
||
{% set model = log.user_agent.split('Build/')[0].split(';')[-1].strip() %}
|
||
{{ model if model else 'HarmonyOS NEXT' }}
|
||
{% else %}
|
||
HarmonyOS NEXT
|
||
{% endif %}
|
||
{% elif 'iPhone' in log.user_agent %}
|
||
iPhone{{ log.user_agent.split('iPhone ')[1].split(' ')[0] if 'iPhone ' in log.user_agent else '' }}
|
||
{% elif 'iPad' in log.user_agent %}
|
||
iPad{{ log.user_agent.split('iPad ')[1].split(' ')[0] if 'iPad ' in log.user_agent else '' }}
|
||
{% else %}
|
||
HarmonyOS NEXT
|
||
{% endif %}
|
||
{% else %}
|
||
{% if 'Windows' in log.user_agent %}
|
||
Windows{% if 'Win64' in log.user_agent %} 64位{% endif %}
|
||
{% elif 'Macintosh' in log.user_agent %}
|
||
macOS
|
||
{% elif 'Linux' in log.user_agent %}
|
||
Linux{% if 'Ubuntu' in log.user_agent %} (Ubuntu){% elif 'Fedora' in log.user_agent %} (Fedora){% elif 'Debian' in log.user_agent %} (Debian){% endif %}
|
||
{% else %}
|
||
HarmonyOS NEXT
|
||
{% endif %}
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.password-input-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.password-input-wrapper input {
|
||
width: 100%;
|
||
padding: 8px 40px 8px 12px;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.toggle-password {
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
background: none;
|
||
border: none;
|
||
color: #666;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 24px;
|
||
height: 24px;
|
||
z-index: 2;
|
||
}
|
||
|
||
.toggle-password:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.toggle-password:focus {
|
||
outline: none;
|
||
}
|
||
|
||
.password-input-wrapper input:hover {
|
||
border-color: #b2b2b2;
|
||
}
|
||
|
||
.password-input-wrapper input:focus {
|
||
border-color: #0066cc;
|
||
outline: none;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
// 添加通知显示函数
|
||
function showNotification(message, type = 'success') {
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
notification.textContent = message;
|
||
|
||
notification.style.position = 'fixed';
|
||
notification.style.bottom = '20px';
|
||
notification.style.right = '20px';
|
||
notification.style.padding = '10px 20px';
|
||
notification.style.borderRadius = '4px';
|
||
notification.style.backgroundColor = type === 'success' ? '#4CAF50' : '#f44336';
|
||
notification.style.color = 'white';
|
||
notification.style.zIndex = '1000';
|
||
notification.style.animation = 'fadeInOut 3s forwards';
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
setTimeout(() => {
|
||
notification.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
// 修改添加管理员函数
|
||
function submitAddAdminForm(event, form) {
|
||
event.preventDefault();
|
||
const formData = new FormData(form);
|
||
|
||
fetch('{{ url_for("add_admin") }}', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification(data.message, 'success');
|
||
form.reset();
|
||
// 刷新页面以显示新管理员
|
||
location.reload();
|
||
} else {
|
||
showNotification(data.error, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showNotification('操作失败,请重试', 'error');
|
||
console.error('Error:', error);
|
||
});
|
||
}
|
||
|
||
// 修改删除管理员函数
|
||
function deleteAdmin(adminId) {
|
||
if (!confirm('确定要删除这个管理员吗?')) return;
|
||
|
||
fetch(`/delete_admin/${adminId}`, {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification(data.message, 'success');
|
||
// 使用更可靠的选择器
|
||
const adminRows = document.querySelectorAll('tr');
|
||
adminRows.forEach(row => {
|
||
// 检查行中是否包含带有特定 onclick 属性的删除按钮
|
||
const deleteButton = row.querySelector(`a[onclick*="deleteAdmin(${adminId})"]`);
|
||
if (deleteButton) {
|
||
row.remove();
|
||
// 同时删除对应的日志行
|
||
const logsRow = document.getElementById(`logs-${adminId}`);
|
||
if (logsRow) {
|
||
logsRow.remove();
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
showNotification(data.error, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showNotification('删除失败,请重试', 'error');
|
||
console.error('Error:', error);
|
||
});
|
||
}
|
||
|
||
// 修改日志查看函数
|
||
function showLogs(adminId) {
|
||
const logsRow = document.getElementById(`logs-${adminId}`);
|
||
if (logsRow.style.display === 'none') {
|
||
logsRow.style.display = 'table-row';
|
||
|
||
// 平滑滚动到日志行
|
||
const rect = logsRow.getBoundingClientRect();
|
||
const absoluteTop = window.pageYOffset + rect.top - 100; // 减去100px的偏移量,让视图更好
|
||
window.scrollTo({
|
||
top: absoluteTop,
|
||
behavior: 'smooth'
|
||
});
|
||
} else {
|
||
logsRow.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 添加密码显示/隐藏功能
|
||
function togglePassword(inputId) {
|
||
const input = document.getElementById(inputId);
|
||
const button = input.nextElementSibling;
|
||
const icon = button.querySelector('i');
|
||
|
||
if (input.type === 'password') {
|
||
input.type = 'text';
|
||
icon.className = 'fas fa-eye-slash';
|
||
} else {
|
||
input.type = 'password';
|
||
icon.className = 'fas fa-eye';
|
||
}
|
||
}
|
||
|
||
// 添加日期格式化过滤器
|
||
function formatDateTime(dateStr) {
|
||
if (!dateStr || dateStr === '从未登录') return dateStr;
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||
}
|
||
|
||
// 添加动画样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes fadeInOut {
|
||
0% { opacity: 0; transform: translateY(20px); }
|
||
10% { opacity: 1; transform: translateY(0); }
|
||
90% { opacity: 1; transform: translateY(0); }
|
||
100% { opacity: 0; transform: translateY(-20px); }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
</script>
|
||
{% endblock %} |