629 lines
23 KiB
HTML
629 lines
23 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 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
|
|
});
|
|
|
|
apiRequest(`/api/v1/admins?${params.toString()}`, {
|
|
method: 'GET'
|
|
})
|
|
.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 = '«';
|
|
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 = '»';
|
|
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) {
|
|
apiRequest(`/api/v1/admins/${adminId}`, {
|
|
method: 'GET'
|
|
})
|
|
.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;
|
|
}
|
|
|
|
apiRequest(`/api/v1/admins/${adminId}/toggle-status`, {
|
|
method: 'POST'
|
|
})
|
|
.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;
|
|
}
|
|
|
|
apiRequest(`/api/v1/admins/${adminId}`, {
|
|
method: 'DELETE'
|
|
})
|
|
.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';
|
|
|
|
apiRequest(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 %}
|