Kamixitong/app/web/templates/admin/list.html
2025-11-11 21:39:12 +08:00

661 lines
24 KiB
HTML

{% extends "base.html" %}
{% block title %}账号管理 - 软件授权管理系统{% endblock %}
{% block page_title %}账号管理{% endblock %}
{% block page_actions %}
<div class="btn-toolbar mb-2 mb-md-0">
<button type="button" class="btn btn-sm btn-outline-primary" id="create-admin-btn">
<i class="fas fa-plus me-1"></i>
创建账号
</button>
</div>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<!-- 搜索和筛选 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form">
<div class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control" id="search-keyword" placeholder="用户名搜索...">
</div>
<div class="col-md-3">
<select class="form-select" id="search-role">
<option value="">全部角色</option>
<option value="0">普通管理员</option>
<option value="1">超级管理员</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="search-status">
<option value="">全部状态</option>
<option value="0">禁用</option>
<option value="1">正常</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary w-100">
<i class="fas fa-search me-2"></i>
搜索
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 账号列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>创建时间</th>
<th>最后登录</th>
<th>操作</th>
</tr>
</thead>
<tbody id="admin-list">
<tr>
<td colspan="7" class="text-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
加载中...
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="账号列表分页">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
</ul>
</nav>
</div>
</div>
</div>
</div>
<!-- 创建/编辑账号模态框 -->
<div class="modal fade" id="adminModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="adminModalLabel">创建账号</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="admin-form">
<input type="hidden" id="admin-id">
<div class="mb-3">
<label for="username" class="form-label">用户名 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="username" required>
<div class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="email" class="form-label">邮箱</label>
<input type="email" class="form-control" id="email">
<div class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="password" class="form-label" id="password-label">密码 <span class="text-danger">*</span></label>
<input type="password" class="form-control" id="password">
<div class="form-text" id="password-help">创建新账号时必填,编辑时留空则不修改密码</div>
<div class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="role" class="form-label">角色</label>
<select class="form-select" id="role">
<option value="0">普通管理员</option>
<option value="1">超级管理员</option>
</select>
<div class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="status" class="form-label">状态</label>
<select class="form-select" id="status">
<option value="1">正常</option>
<option value="0">禁用</option>
</select>
<div class="invalid-feedback"></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="save-admin-btn" data-loading-text="保存中...">保存</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
(function() {
'use strict';
// 状态管理
const state = {
currentPage: 1,
currentEditId: null,
searchParams: {
keyword: '',
role: '',
status: ''
},
isLoading: false
};
// DOM元素缓存
const elements = {
searchForm: document.getElementById('search-form'),
searchKeyword: document.getElementById('search-keyword'),
searchRole: document.getElementById('search-role'),
searchStatus: document.getElementById('search-status'),
adminList: document.getElementById('admin-list'),
pagination: document.getElementById('pagination'),
createBtn: document.getElementById('create-admin-btn'),
saveBtn: document.getElementById('save-admin-btn'),
adminModal: document.getElementById('adminModal'),
adminForm: document.getElementById('admin-form'),
adminModalLabel: document.getElementById('adminModalLabel'),
passwordLabel: document.getElementById('password-label'),
passwordHelp: document.getElementById('password-help')
};
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
init();
});
function init() {
bindEvents();
loadAdmins(1);
}
function bindEvents() {
// 搜索表单
elements.searchForm.addEventListener('submit', function(e) {
e.preventDefault();
state.searchParams = {
keyword: elements.searchKeyword.value.trim(),
role: elements.searchRole.value,
status: elements.searchStatus.value
};
loadAdmins(1);
});
// 创建账号按钮
elements.createBtn.addEventListener('click', function() {
showAdminModal();
});
// 保存账号按钮
elements.saveBtn.addEventListener('click', function() {
saveAdmin();
});
// 模态框关闭时重置表单
elements.adminModal.addEventListener('hidden.bs.modal', function() {
resetForm();
});
// 事件委托处理列表操作
elements.adminList.addEventListener('click', function(e) {
const target = e.target.closest('button');
if (!target) return;
const adminId = target.getAttribute('data-id');
if (!adminId) return;
if (target.classList.contains('edit-btn')) {
editAdmin(adminId);
} else if (target.classList.contains('delete-btn')) {
const username = target.getAttribute('data-username');
deleteAdmin(adminId, username);
} else if (target.classList.contains('toggle-status-btn')) {
const currentStatus = parseInt(target.getAttribute('data-status'));
toggleAdminStatus(adminId, currentStatus);
}
});
}
function setLoading(loading) {
state.isLoading = loading;
if (loading) {
elements.saveBtn.disabled = true;
elements.saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status"></span>保存中...';
} else {
elements.saveBtn.disabled = false;
elements.saveBtn.innerHTML = '保存';
}
}
function showNotification(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.top = '20px';
alertDiv.style.right = '20px';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
function showAdminModal(admin = null) {
resetFormValidation();
if (admin) {
// 编辑模式
state.currentEditId = admin.admin_id;
elements.adminModalLabel.textContent = '编辑账号';
elements.adminForm.querySelector('#admin-id').value = admin.admin_id;
elements.adminForm.querySelector('#username').value = admin.username;
elements.adminForm.querySelector('#email').value = admin.email || '';
elements.adminForm.querySelector('#password').value = '';
elements.adminForm.querySelector('#role').value = admin.role;
elements.adminForm.querySelector('#status').value = admin.status;
elements.passwordLabel.innerHTML = '密码';
elements.passwordHelp.textContent = '留空则不修改密码';
} else {
// 创建模式
state.currentEditId = null;
elements.adminModalLabel.textContent = '创建账号';
elements.adminForm.reset();
elements.adminForm.querySelector('#admin-id').value = '';
elements.passwordLabel.innerHTML = '密码 <span class="text-danger">*</span>';
elements.passwordHelp.textContent = '创建新账号时必填';
}
const modal = new bootstrap.Modal(elements.adminModal);
modal.show();
}
function resetForm() {
elements.adminForm.reset();
resetFormValidation();
state.currentEditId = null;
}
function resetFormValidation() {
const inputs = elements.adminForm.querySelectorAll('input, select');
inputs.forEach(input => {
input.classList.remove('is-invalid');
const feedback = input.parentElement.querySelector('.invalid-feedback');
if (feedback) {
feedback.textContent = '';
}
});
}
function showFieldError(fieldId, message) {
const field = document.getElementById(fieldId);
const feedback = field.parentElement.querySelector('.invalid-feedback');
field.classList.add('is-invalid');
if (feedback) {
feedback.textContent = message;
}
}
function loadAdmins(page = 1) {
if (state.isLoading) return;
state.currentPage = page;
const params = new URLSearchParams({
page: page,
per_page: 10,
keyword: state.searchParams.keyword,
role: state.searchParams.role,
status: state.searchParams.status
});
fetch(`/api/v1/admins?${params.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
renderAdminList(data.data.admins);
renderPagination(data.data.pagination);
} else {
showNotification('加载账号列表失败: ' + data.message, 'danger');
}
})
.catch(error => {
console.error('Failed to load admins:', error);
showNotification('加载账号列表失败,请检查网络连接', 'danger');
});
}
function renderAdminList(admins) {
const tbody = elements.adminList;
tbody.innerHTML = '';
if (admins.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">暂无数据</td></tr>';
return;
}
const fragment = document.createDocumentFragment();
admins.forEach(admin => {
const tr = document.createElement('tr');
const usernameTd = document.createElement('td');
usernameTd.textContent = admin.username;
tr.appendChild(usernameTd);
const emailTd = document.createElement('td');
emailTd.textContent = admin.email || '-';
tr.appendChild(emailTd);
const roleTd = document.createElement('td');
const roleSpan = document.createElement('span');
roleSpan.className = `badge bg-${admin.role === 1 ? 'primary' : 'secondary'}`;
roleSpan.textContent = admin.role_name;
roleTd.appendChild(roleSpan);
tr.appendChild(roleTd);
const statusTd = document.createElement('td');
const statusSpan = document.createElement('span');
statusSpan.className = `badge bg-${admin.status === 1 ? 'success' : 'danger'}`;
statusSpan.textContent = admin.status_name;
statusTd.appendChild(statusSpan);
tr.appendChild(statusTd);
const createTimeTd = document.createElement('td');
createTimeTd.textContent = admin.create_time || '-';
tr.appendChild(createTimeTd);
const lastLoginTd = document.createElement('td');
lastLoginTd.textContent = admin.last_login_time || '-';
tr.appendChild(lastLoginTd);
const actionTd = document.createElement('td');
// 状态切换按钮
const toggleBtn = document.createElement('button');
toggleBtn.className = `btn btn-sm btn-outline-${admin.status === 1 ? 'danger' : 'success'} toggle-status-btn`;
toggleBtn.setAttribute('data-id', admin.admin_id);
toggleBtn.setAttribute('data-status', admin.status);
toggleBtn.setAttribute('title', admin.status === 1 ? '禁用账号' : '启用账号');
toggleBtn.innerHTML = `<i class="fas fa-${admin.status === 1 ? 'times' : 'check'}"></i>`;
actionTd.appendChild(toggleBtn);
// 编辑按钮
const editBtn = document.createElement('button');
editBtn.className = 'btn btn-sm btn-outline-primary edit-btn';
editBtn.setAttribute('data-id', admin.admin_id);
editBtn.setAttribute('title', '编辑账号');
editBtn.innerHTML = '<i class="fas fa-edit"></i>';
actionTd.appendChild(editBtn);
// 删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-sm btn-outline-danger delete-btn';
deleteBtn.setAttribute('data-id', admin.admin_id);
deleteBtn.setAttribute('data-username', admin.username);
deleteBtn.setAttribute('title', '删除账号');
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
actionTd.appendChild(deleteBtn);
tr.appendChild(actionTd);
fragment.appendChild(tr);
});
tbody.appendChild(fragment);
}
function renderPagination(pagination) {
const ul = elements.pagination;
ul.innerHTML = '';
if (pagination.pages <= 1) {
return;
}
const fragment = document.createDocumentFragment();
// 上一页
const prevLi = document.createElement('li');
prevLi.className = `page-item ${pagination.has_prev ? '' : 'disabled'}`;
const prevLink = document.createElement('a');
prevLink.className = 'page-link';
prevLink.href = '#';
prevLink.innerHTML = '&laquo;';
if (pagination.has_prev) {
prevLink.addEventListener('click', function(e) {
e.preventDefault();
loadAdmins(pagination.page - 1);
});
}
prevLi.appendChild(prevLink);
fragment.appendChild(prevLi);
// 页码
for (let i = 1; i <= pagination.pages; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === pagination.page ? 'active' : ''}`;
const link = document.createElement('a');
link.className = 'page-link';
link.href = '#';
link.textContent = i;
if (i !== pagination.page) {
link.addEventListener('click', function(e) {
e.preventDefault();
loadAdmins(i);
});
}
li.appendChild(link);
fragment.appendChild(li);
}
// 下一页
const nextLi = document.createElement('li');
nextLi.className = `page-item ${pagination.has_next ? '' : 'disabled'}`;
const nextLink = document.createElement('a');
nextLink.className = 'page-link';
nextLink.href = '#';
nextLink.innerHTML = '&raquo;';
if (pagination.has_next) {
nextLink.addEventListener('click', function(e) {
e.preventDefault();
loadAdmins(pagination.page + 1);
});
}
nextLi.appendChild(nextLink);
fragment.appendChild(nextLi);
ul.appendChild(fragment);
}
function editAdmin(adminId) {
fetch(`/api/v1/admins/${adminId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAdminModal(data.data);
} else {
showNotification('获取账号信息失败: ' + data.message, 'danger');
}
})
.catch(error => {
console.error('Failed to get admin:', error);
showNotification('获取账号信息失败,请检查网络连接', 'danger');
});
}
function toggleAdminStatus(adminId, currentStatus) {
const action = currentStatus === 1 ? '禁用' : '启用';
if (!confirm(`确定要${action}此账号吗?`)) {
return;
}
fetch(`/api/v1/admins/${adminId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
// 重新加载列表以确保数据一致性
loadAdmins(state.currentPage);
} else {
showNotification('切换账号状态失败: ' + data.message, 'danger');
}
})
.catch(error => {
console.error('Failed to toggle admin status:', error);
showNotification('切换账号状态失败,请检查网络连接', 'danger');
});
}
function deleteAdmin(adminId, username) {
if (!confirm(`确定要删除账号 "${username}" 吗?此操作将标记为已删除(软删除),无法恢复!`)) {
return;
}
fetch(`/api/v1/admins/${adminId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
// 重新加载列表
loadAdmins(state.currentPage);
} else {
showNotification('删除账号失败: ' + data.message, 'danger');
}
})
.catch(error => {
console.error('Failed to delete admin:', error);
showNotification('删除账号失败,请检查网络连接', 'danger');
});
}
function saveAdmin() {
if (state.isLoading) return;
resetFormValidation();
const formData = {
username: elements.adminForm.querySelector('#username').value.trim(),
email: elements.adminForm.querySelector('#email').value.trim(),
role: parseInt(elements.adminForm.querySelector('#role').value),
status: parseInt(elements.adminForm.querySelector('#status').value)
};
const password = elements.adminForm.querySelector('#password').value;
// 前端验证
let hasError = false;
if (!formData.username) {
showFieldError('username', '用户名不能为空');
hasError = true;
} else if (formData.username.length < 3) {
showFieldError('username', '用户名至少3个字符');
hasError = true;
}
if (!state.currentEditId && !password) {
showFieldError('password', '密码不能为空');
hasError = true;
} else if (password && password.length < 6) {
showFieldError('password', '密码长度至少6位');
hasError = true;
}
if (hasError) {
return;
}
if (password) {
formData.password = password;
}
setLoading(true);
const isCreate = !state.currentEditId;
const url = isCreate ? '/api/v1/admins' : `/api/v1/admins/${state.currentEditId}`;
const method = isCreate ? 'POST' : 'PUT';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
setLoading(false);
if (data.success) {
showNotification(isCreate ? '账号创建成功' : '账号更新成功', 'success');
const modal = bootstrap.Modal.getInstance(elements.adminModal);
modal.hide();
loadAdmins(state.currentPage);
} else {
// 显示服务器端验证错误
if (data.message.includes('用户名')) {
showFieldError('username', data.message);
} else if (data.message.includes('密码')) {
showFieldError('password', data.message);
} else {
showNotification(isCreate ? '账号创建失败: ' + data.message : '账号更新失败: ' + data.message, 'danger');
}
}
})
.catch(error => {
setLoading(false);
console.error('Failed to save admin:', error);
showNotification(isCreate ? '账号创建失败,请检查网络连接' : '账号更新失败,请检查网络连接', 'danger');
});
}
})();
</script>
{% endblock %}