feat: 用户登录/管理 + 操作日志模块
- tb_user 用户表、tb_log 日志表 - Flask-Login 认证(login/logout/权限装饰器) - 用户管理页(admin 专有):增删改查、改密、角色设置 - 操作日志页:分页查询、按用户/类型筛选 - 测试操作区指令自动记录日志 - 所有页面加 @login_required 保护 - 默认管理员 admin/admin123(首次启动自动创建)
This commit is contained in:
@@ -10,6 +10,16 @@
|
||||
<nav class="top-menu">
|
||||
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
||||
<a href="/test-data" class="{% if request.path == '/test-data' %}active{% endif %}">测试信息</a>
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||
<a href="/logs/" class="{% if request.path == '/logs/' %}active{% endif %}">操作日志</a>
|
||||
<a href="/users/" class="{% if request.path == '/users/' %}active{% endif %}">用户管理</a>
|
||||
{% endif %}
|
||||
<span class="user-info">
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ current_user.username }} ({{ current_user.role }})
|
||||
<a href="/logout">退出</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</nav>
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
36
edc-web/app/templates/login.html
Normal file
36
edc-web/app/templates/login.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - EDC 工装管理系统</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f6fa; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
||||
.login-box { background: #fff; padding: 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,.1); width: 360px; }
|
||||
.login-box h2 { text-align: center; margin-bottom: 24px; color: #2c3e50; }
|
||||
.login-box label { display: block; font-size: 13px; margin-bottom: 4px; color: #555; }
|
||||
.login-box input { width: 100%; padding: 10px 12px; margin-bottom: 16px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
.login-box button { width: 100%; padding: 10px; background: #3498db; color: #fff; border: none; border-radius: 6px; font-size: 15px; cursor: pointer; }
|
||||
.login-box button:hover { background: #2980b9; }
|
||||
.flash-error { color: #e74c3c; font-size: 13px; text-align: center; margin-bottom: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>EDC 工装管理系统</h2>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash-error">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<label>用户名</label>
|
||||
<input type="text" name="username" required autofocus>
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
92
edc-web/app/templates/logs.html
Normal file
92
edc-web/app/templates/logs.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}操作日志 - EDC 工装管理系统{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>操作日志</h2>
|
||||
|
||||
<div class="search-bar">
|
||||
<label>用户名:<input type="text" id="search-username" placeholder="筛选用户..."></label>
|
||||
<label>操作类型:
|
||||
<select id="search-action">
|
||||
<option value="">全部</option>
|
||||
<option value="login">登录</option>
|
||||
<option value="logout">登出</option>
|
||||
<option value="command">指令操作</option>
|
||||
</select>
|
||||
</label>
|
||||
<button onclick="searchLogs(1)" class="btn-search">查询</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户</th>
|
||||
<th>操作类型</th>
|
||||
<th>对象</th>
|
||||
<th>详情</th>
|
||||
<th>结果</th>
|
||||
<th>IP</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="log-tbody"></tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination" id="pagination"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentPage = 1, totalPages = 1;
|
||||
|
||||
async function searchLogs(page = 1) {
|
||||
currentPage = page;
|
||||
const username = document.getElementById("search-username").value;
|
||||
const action_type = document.getElementById("search-action").value;
|
||||
const params = new URLSearchParams({page, per_page: 30});
|
||||
if (username) params.set("username", username);
|
||||
if (action_type) params.set("action_type", action_type);
|
||||
|
||||
const resp = await fetch(`/logs/api/logs?${params}`);
|
||||
const data = await resp.json();
|
||||
renderTable(data.records);
|
||||
totalPages = data.pages;
|
||||
renderPagination();
|
||||
}
|
||||
|
||||
function renderTable(records) {
|
||||
const tbody = document.getElementById("log-tbody");
|
||||
if (!records.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#999;">暂无记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = records.map(r => `
|
||||
<tr>
|
||||
<td>${r.id}</td>
|
||||
<td>${r.username || '-'}</td>
|
||||
<td>${r.action_type}</td>
|
||||
<td>${r.target || '-'}</td>
|
||||
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(r.detail)}">${r.detail || '-'}</td>
|
||||
<td style="color:${r.result==='ok'?'#27ae60':'#e74c3c'}">${r.result}</td>
|
||||
<td>${r.ip || '-'}</td>
|
||||
<td>${r.create_time || '-'}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const div = document.getElementById("pagination");
|
||||
let html = `<button onclick="searchLogs(${currentPage-1})" ${currentPage<=1?'disabled':''}>上一页</button>`;
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `<button onclick="searchLogs(${i})" class="${i===currentPage?'active':''}">${i}</button>`;
|
||||
}
|
||||
html += `<button onclick="searchLogs(${currentPage+1})" ${currentPage>=totalPages?'disabled':''}>下一页</button>`;
|
||||
div.innerHTML = html;
|
||||
}
|
||||
|
||||
function esc(s) { return s.replace(/"/g, '"'); }
|
||||
|
||||
searchLogs(1);
|
||||
</script>
|
||||
{% endblock %}
|
||||
112
edc-web/app/templates/users.html
Normal file
112
edc-web/app/templates/users.html
Normal file
@@ -0,0 +1,112 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}用户管理 - EDC 工装管理系统{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>用户管理</h2>
|
||||
|
||||
<div style="margin-bottom:16px;">
|
||||
<button onclick="showAddForm()" class="btn-search">新增用户</button>
|
||||
<div id="add-form" style="display:none;margin-top:12px;background:#fff;padding:16px;border-radius:8px;">
|
||||
<label>用户名:<input type="text" id="new-username" style="margin:0 8px;"></label>
|
||||
<label>密码:<input type="password" id="new-password" style="margin:0 8px;"></label>
|
||||
<label>角色:
|
||||
<select id="new-role" style="margin:0 8px;">
|
||||
<option value="operator">operator</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
</label>
|
||||
<button onclick="addUser()" class="btn-search">确认</button>
|
||||
<button onclick="document.getElementById('add-form').style.display='none'" class="btn-export">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>角色</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="user-tbody"></tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function loadUsers() {
|
||||
const resp = await fetch("/users/api/users");
|
||||
const users = await resp.json();
|
||||
const tbody = document.getElementById("user-tbody");
|
||||
tbody.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${u.id}</td>
|
||||
<td>${u.username}</td>
|
||||
<td>${u.role}</td>
|
||||
<td>${u.is_active ? '启用' : '禁用'}</td>
|
||||
<td>${u.create_time || '-'}</td>
|
||||
<td>
|
||||
<select onchange="updateUser(${u.id}, this, 'role')" data-field="role">
|
||||
<option value="operator" ${u.role==='operator'?'selected':''}>operator</option>
|
||||
<option value="admin" ${u.role==='admin'?'selected':''}>admin</option>
|
||||
</select>
|
||||
<select onchange="updateUser(${u.id}, this, 'is_active')" data-field="is_active">
|
||||
<option value="1" ${u.is_active?'selected':''}>启用</option>
|
||||
<option value="0" ${!u.is_active?'selected':''}>禁用</option>
|
||||
</select>
|
||||
<button onclick="resetPwd(${u.id})" class="btn-search" style="font-size:11px;">改密</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function showAddForm() {
|
||||
document.getElementById("add-form").style.display = "block";
|
||||
document.getElementById("new-username").value = "";
|
||||
document.getElementById("new-password").value = "";
|
||||
}
|
||||
|
||||
async function addUser() {
|
||||
const username = document.getElementById("new-username").value.trim();
|
||||
const password = document.getElementById("new-password").value;
|
||||
const role = document.getElementById("new-role").value;
|
||||
if (!username || !password) { alert("用户名和密码不能为空"); return; }
|
||||
const resp = await fetch("/users/api/users", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({username, password, role}),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.ok) { document.getElementById("add-form").style.display = "none"; loadUsers(); }
|
||||
else { alert(data.error || "创建失败"); }
|
||||
}
|
||||
|
||||
async function updateUser(id, el, field) {
|
||||
const value = el.value;
|
||||
const body = {};
|
||||
body[field] = field === 'is_active' ? parseInt(value) : value;
|
||||
await fetch(`/users/api/users/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
async function resetPwd(id) {
|
||||
const pwd = prompt("输入新密码(至少6位):");
|
||||
if (!pwd || pwd.length < 6) { alert("密码至少6位"); return; }
|
||||
await fetch(`/users/api/users/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({password: pwd}),
|
||||
});
|
||||
alert("密码已更新");
|
||||
}
|
||||
|
||||
loadUsers();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user