- auth.py: 新增 privileged_required 装饰器 (admin+manager),admin_required 仅限用户管理 - 路由权限: fixture/logs/device_logs/test_data 的 admin 检查改为 admin+manager - 前端: 导航栏/删除按钮/配置按钮扩展为 admin+manager 可见 - 用户管理: 角色下拉增加 manager 选项,仍仅 admin 可访问 - 新增 /change-password 路由+模板,所有登录用户可自行修改密码 - edc_server models.py: role COMMENT 更新 + ALTER TABLE 迁移
192 lines
7.8 KiB
HTML
192 lines
7.8 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}设备日志 - EDC 工装管理系统{% endblock %}
|
||
|
||
{% block content %}
|
||
<h2>设备事件日志</h2>
|
||
|
||
<div class="search-bar">
|
||
<label>设备序列号:<input type="text" id="search-serial" placeholder="筛选设备..."></label>
|
||
<label>事件类型:
|
||
<select id="search-event">
|
||
<option value="">全部</option>
|
||
<option value="login">登录</option>
|
||
<option value="online">在线</option>
|
||
<option value="offline">离线</option>
|
||
<option value="poor">通信不良</option>
|
||
<option value="tcp_connect">TCP连接</option>
|
||
<option value="tcp_disconnect">TCP断开</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
时间范围:
|
||
<input type="date" id="search-date-from">
|
||
<input type="time" id="search-time-from" step="1" style="width:110px;" title="起始时间(时:分:秒)">
|
||
至
|
||
<input type="date" id="search-date-to">
|
||
<input type="time" id="search-time-to" step="1" style="width:110px;" title="截止时间(时:分:秒)">
|
||
</label>
|
||
<button onclick="searchLogs(1)" class="btn-search">查询</button>
|
||
<button onclick="exportCSV()" class="btn-export">导出 CSV</button>
|
||
{% if current_user.role in ('admin', 'manager') %}
|
||
<button onclick="confirmDeleteLogs()" class="btn-delete">🗑 删除</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>设备序列号</th>
|
||
<th>设备IP</th>
|
||
<th>事件类型</th>
|
||
<th>事件内容</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;
|
||
|
||
function getDatetime(dateId, timeId) {
|
||
const d = document.getElementById(dateId).value;
|
||
const t = document.getElementById(timeId).value;
|
||
if (!d) return "";
|
||
if (!t) return d; // 纯日期 → 后端自动补时间
|
||
return d + " " + t; // 完整 datetime
|
||
}
|
||
|
||
async function searchLogs(page = 1) {
|
||
currentPage = page;
|
||
const serial = document.getElementById("search-serial").value;
|
||
const event_type = document.getElementById("search-event").value;
|
||
const date_from = getDatetime("search-date-from", "search-time-from");
|
||
const date_to = getDatetime("search-date-to", "search-time-to");
|
||
const params = new URLSearchParams({page, per_page: 30});
|
||
if (serial) params.set("serial", serial);
|
||
if (event_type) params.set("event_type", event_type);
|
||
if (date_from) params.set("date_from", date_from);
|
||
if (date_to) params.set("date_to", date_to);
|
||
|
||
const resp = await fetch(`/api/device-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="6" style="text-align:center;color:#999;">暂无记录</td></tr>';
|
||
return;
|
||
}
|
||
tbody.innerHTML = records.map(r => {
|
||
let typeStyle = '';
|
||
if (r.event_type === 'online' || r.event_type === 'login') typeStyle = 'color:#27ae60;font-weight:bold;';
|
||
else if (r.event_type === 'offline') typeStyle = 'color:#e74c3c;font-weight:bold;';
|
||
else if (r.event_type === 'poor') typeStyle = 'color:#f39c12;font-weight:bold;';
|
||
else if (r.event_type === 'tcp_disconnect') typeStyle = 'color:#e74c3c;';
|
||
else if (r.event_type === 'tcp_connect') typeStyle = 'color:#3498db;';
|
||
return `
|
||
<tr>
|
||
<td>${r.id}</td>
|
||
<td>${escHtml(r.device_serial || '-')}</td>
|
||
<td>${r.device_ip || '-'}</td>
|
||
<td style="${typeStyle}">${eventLabel(r.event_type)}</td>
|
||
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.event_content || '')}">${r.event_content || '-'}</td>
|
||
<td>${fmtTime(r.create_time)}</td>
|
||
</tr>`;
|
||
}).join("");
|
||
}
|
||
|
||
function eventLabel(t) {
|
||
const m = {login: '登录', online: '在线', offline: '离线', poor: '通信不良',
|
||
tcp_connect: 'TCP连接', tcp_disconnect: 'TCP断开'};
|
||
return m[t] || t;
|
||
}
|
||
|
||
function fmtTime(v) {
|
||
if (!v) return '-';
|
||
// Flask jsonify 给 MySQL DATETIME 加 "GMT" 后缀,但实际值是服务器本地时间(UTC+8)
|
||
// 去掉 "GMT" 让 JS 按本地时间解析,避免时区偏移 8 小时
|
||
const cleaned = String(v).replace(/ GMT$/, '');
|
||
const d = new Date(cleaned);
|
||
if (isNaN(d.getTime())) return String(v).substring(0, 19);
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||
const d2 = String(d.getDate()).padStart(2, '0');
|
||
const h = String(d.getHours()).padStart(2, '0');
|
||
const min = String(d.getMinutes()).padStart(2, '0');
|
||
const s = String(d.getSeconds()).padStart(2, '0');
|
||
return `${y}-${m}-${d2} ${h}:${min}:${s}`;
|
||
}
|
||
|
||
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 escHtml(s) {
|
||
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
// ─── 导出 CSV ────────────────────────────────────
|
||
|
||
function exportCSV() {
|
||
const serial = document.getElementById("search-serial").value;
|
||
const event_type = document.getElementById("search-event").value;
|
||
const date_from = getDatetime("search-date-from", "search-time-from");
|
||
const date_to = getDatetime("search-date-to", "search-time-to");
|
||
const params = new URLSearchParams();
|
||
if (serial) params.set("serial", serial);
|
||
if (event_type) params.set("event_type", event_type);
|
||
if (date_from) params.set("date_from", date_from);
|
||
if (date_to) params.set("date_to", date_to);
|
||
|
||
// 直接打开下载链接
|
||
window.location.href = `/api/device-logs/export?${params}`;
|
||
}
|
||
|
||
// ─── 删除 ────────────────────────────────────────
|
||
|
||
async function confirmDeleteLogs() {
|
||
const serial = document.getElementById("search-serial").value;
|
||
const event_type = document.getElementById("search-event").value;
|
||
const date_from = getDatetime("search-date-from", "search-time-from");
|
||
const date_to = getDatetime("search-date-to", "search-time-to");
|
||
if (!serial && !event_type && !date_from && !date_to) {
|
||
alert("请至少输入设备序列号、选择事件类型或指定时间范围作为删除条件");
|
||
return;
|
||
}
|
||
const msg = `确认删除设备日志?\n条件: serial=${serial || '(无)'} type=${event_type || '(无)'} time=${date_from||'(无)'}~${date_to||'(无)'}\n此操作不可撤销!`;
|
||
if (!confirm(msg)) return;
|
||
|
||
const resp = await fetch("/api/device-logs/delete", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({serial, event_type, date_from, date_to}),
|
||
});
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
alert(`已删除 ${data.deleted} 条记录`);
|
||
searchLogs(1);
|
||
} else {
|
||
alert("删除失败: " + (data.error || "未知错误"));
|
||
}
|
||
}
|
||
|
||
searchLogs(1);
|
||
</script>
|
||
{% endblock %}
|