feat: edc-web 设备日志管理页 + 在线状态实时刷新
- 新增 /device-logs 设备事件日志管理页 (admin 权限) - 支持按设备序列号/事件类型筛选查询 - 支持 admin 按条件删除日志 - 不同事件类型彩色标识 (在线=绿, 离线=红, 通信不良=橙) - 新增 /api/devices/<id>/status 设备状态 API - 设备列表页:每 5s 异步刷新所有设备在线状态 - 测试操作页:顶部显示设备状态,每 5s 异步刷新 - dnt_info state 支持三态显示 (在线/离线/通信不良) - 导航栏增加「设备日志」入口 (admin only)
This commit is contained in:
@@ -20,6 +20,7 @@ def create_app() -> Flask:
|
|||||||
from app.routes.fixture import bp as fixture_bp
|
from app.routes.fixture import bp as fixture_bp
|
||||||
from app.routes.users import bp as users_bp
|
from app.routes.users import bp as users_bp
|
||||||
from app.routes.logs import bp as logs_bp
|
from app.routes.logs import bp as logs_bp
|
||||||
|
from app.routes.device_logs import bp as device_logs_bp
|
||||||
|
|
||||||
app.register_blueprint(devices_bp)
|
app.register_blueprint(devices_bp)
|
||||||
app.register_blueprint(test_op_bp)
|
app.register_blueprint(test_op_bp)
|
||||||
@@ -27,6 +28,7 @@ def create_app() -> Flask:
|
|||||||
app.register_blueprint(fixture_bp)
|
app.register_blueprint(fixture_bp)
|
||||||
app.register_blueprint(users_bp)
|
app.register_blueprint(users_bp)
|
||||||
app.register_blueprint(logs_bp)
|
app.register_blueprint(logs_bp)
|
||||||
|
app.register_blueprint(device_logs_bp)
|
||||||
|
|
||||||
# 初始化默认管理员
|
# 初始化默认管理员
|
||||||
_ensure_admin()
|
_ensure_admin()
|
||||||
|
|||||||
@@ -827,3 +827,79 @@ def delete_test_data(serial: str = "", date_from: str = "",
|
|||||||
return cnt
|
return cnt
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── tb_device_log ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_device_logs(page: int = 1, per_page: int = 30,
|
||||||
|
serial: str = "", event_type: str = "") -> tuple[list[dict], int]:
|
||||||
|
"""分页查询设备事件日志,返回 (records, total)"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
where = []
|
||||||
|
params = []
|
||||||
|
if serial:
|
||||||
|
where.append("device_serial LIKE %s")
|
||||||
|
params.append(f"%{serial}%")
|
||||||
|
if event_type:
|
||||||
|
where.append("event_type = %s")
|
||||||
|
params.append(event_type)
|
||||||
|
|
||||||
|
where_clause = " AND ".join(where) if where else "1=1"
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT COUNT(*) as total FROM tb_device_log WHERE {where_clause}",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
total = cur.fetchone()["total"]
|
||||||
|
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT * FROM tb_device_log WHERE {where_clause} "
|
||||||
|
f"ORDER BY id DESC LIMIT %s OFFSET %s",
|
||||||
|
params + [per_page, offset],
|
||||||
|
)
|
||||||
|
return cur.fetchall(), total
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_device_logs(serial: str = "", event_type: str = "",
|
||||||
|
date_from: str = "", date_to: str = "") -> int:
|
||||||
|
"""删除符合条件的设备日志,返回删除行数。至少需要一个条件。"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
where = []
|
||||||
|
params = []
|
||||||
|
if serial:
|
||||||
|
where.append("device_serial LIKE %s")
|
||||||
|
params.append(f"%{serial}%")
|
||||||
|
if event_type:
|
||||||
|
where.append("event_type = %s")
|
||||||
|
params.append(event_type)
|
||||||
|
if date_from:
|
||||||
|
where.append("create_time >= %s")
|
||||||
|
params.append(date_from if len(date_from) > 10 else date_from)
|
||||||
|
if date_to:
|
||||||
|
where.append("create_time <= %s")
|
||||||
|
params.append(date_to if len(date_to) > 10 else date_to + " 23:59:59")
|
||||||
|
|
||||||
|
if not where:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
where_clause = " AND ".join(where)
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT COUNT(*) as cnt FROM tb_device_log WHERE {where_clause}",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
cnt = cur.fetchone()["cnt"]
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
f"DELETE FROM tb_device_log WHERE {where_clause}", params,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cnt
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
58
edc-web/app/routes/device_logs.py
Normal file
58
edc-web/app/routes/device_logs.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""设备事件日志 API"""
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, render_template, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from app.models import get_device_logs, delete_device_logs, insert_log
|
||||||
|
|
||||||
|
bp = Blueprint("device_logs", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/device-logs")
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
"""设备日志页面"""
|
||||||
|
return render_template("device_logs.html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/device-logs")
|
||||||
|
@login_required
|
||||||
|
def api_device_logs():
|
||||||
|
"""查询设备事件日志"""
|
||||||
|
page = request.args.get("page", 1, type=int)
|
||||||
|
per_page = request.args.get("per_page", 30, type=int)
|
||||||
|
serial = request.args.get("serial", "", type=str)
|
||||||
|
event_type = request.args.get("event_type", "", type=str)
|
||||||
|
|
||||||
|
records, total = get_device_logs(
|
||||||
|
page=page, per_page=per_page,
|
||||||
|
serial=serial, event_type=event_type,
|
||||||
|
)
|
||||||
|
pages = max(1, (total + per_page - 1) // per_page)
|
||||||
|
return jsonify({"records": records, "total": total, "pages": pages})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/device-logs/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_device_logs_delete():
|
||||||
|
"""删除设备日志(admin 权限)"""
|
||||||
|
if current_user.role != "admin":
|
||||||
|
return jsonify({"ok": False, "error": "无权限"}), 403
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
serial = data.get("serial", "")
|
||||||
|
event_type = data.get("event_type", "")
|
||||||
|
date_from = data.get("date_from", "")
|
||||||
|
date_to = data.get("date_to", "")
|
||||||
|
|
||||||
|
deleted = delete_device_logs(
|
||||||
|
serial=serial, event_type=event_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "delete",
|
||||||
|
target="device_log",
|
||||||
|
detail=f"删除 {deleted} 条设备日志 serial={serial} type={event_type}",
|
||||||
|
result="ok",
|
||||||
|
ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True, "deleted": deleted})
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from flask import Blueprint, jsonify, render_template, request
|
from flask import Blueprint, jsonify, render_template, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from app.models import get_all_devices, update_device_name
|
from app.models import get_all_devices, update_device_name, get_device_by_id
|
||||||
|
|
||||||
bp = Blueprint("devices", __name__)
|
bp = Blueprint("devices", __name__)
|
||||||
|
|
||||||
@@ -21,6 +21,22 @@ def api_devices():
|
|||||||
return jsonify(devices)
|
return jsonify(devices)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/devices/<int:device_id>/status")
|
||||||
|
def api_device_status(device_id):
|
||||||
|
"""获取单个设备的在线状态"""
|
||||||
|
device = get_device_by_id(device_id)
|
||||||
|
if not device:
|
||||||
|
return jsonify({"ok": False, "error": "设备不存在"}), 404
|
||||||
|
state_names = {0: "离线", 1: "在线", 2: "通信不良"}
|
||||||
|
return jsonify({
|
||||||
|
"ok": True,
|
||||||
|
"device_id": device_id,
|
||||||
|
"state": device["state"],
|
||||||
|
"state_name": state_names.get(device["state"], "未知"),
|
||||||
|
"serial": device["serial"],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/devices/<int:device_id>/name", methods=["PUT"])
|
@bp.route("/api/devices/<int:device_id>/name", methods=["PUT"])
|
||||||
def api_update_name(device_id):
|
def api_update_name(device_id):
|
||||||
"""修改设备名称"""
|
"""修改设备名称"""
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ tr:hover { background: #f8f9fa; }
|
|||||||
/* === Online Status === */
|
/* === Online Status === */
|
||||||
.status-online { color: #27ae60; font-weight: 600; }
|
.status-online { color: #27ae60; font-weight: 600; }
|
||||||
.status-offline { color: #bdc3c7; }
|
.status-offline { color: #bdc3c7; }
|
||||||
|
.status-poor { color: #f39c12; font-weight: 600; }
|
||||||
|
|
||||||
/* === Editable Name === */
|
/* === Editable Name === */
|
||||||
.editable-name { cursor: pointer; border-bottom: 1px dashed transparent; }
|
.editable-name { cursor: pointer; border-bottom: 1px dashed transparent; }
|
||||||
|
|||||||
@@ -8,15 +8,18 @@ async function loadDevices() {
|
|||||||
|
|
||||||
function renderTable(devices) {
|
function renderTable(devices) {
|
||||||
const tbody = document.querySelector("#device-table tbody");
|
const tbody = document.querySelector("#device-table tbody");
|
||||||
tbody.innerHTML = devices.map(d => `
|
tbody.innerHTML = devices.map(d => {
|
||||||
|
const stateLabel = getStateLabel(d.state);
|
||||||
|
const stateClass = getStateClass(d.state);
|
||||||
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${d.serial}</td>
|
<td>${d.serial}</td>
|
||||||
<td class="editable-name" onclick="editName(${d.id}, '${esc(d.name)}', this)">
|
<td class="editable-name" onclick="editName(${d.id}, '${esc(d.name)}', this)">
|
||||||
${d.name || '(点击编辑)'}
|
${d.name || '(点击编辑)'}
|
||||||
</td>
|
</td>
|
||||||
<td>${d.ip || '-'}</td>
|
<td>${d.ip || '-'}</td>
|
||||||
<td class="${d.state === 1 ? 'status-online' : 'status-offline'}">
|
<td class="${stateClass} status-cell" data-device-id="${d.id}">
|
||||||
${d.state === 1 ? '在线' : '离线'}
|
${stateLabel}
|
||||||
</td>
|
</td>
|
||||||
<td>${d.version || '-'}</td>
|
<td>${d.version || '-'}</td>
|
||||||
<td>${d.last_login || '-'}</td>
|
<td>${d.last_login || '-'}</td>
|
||||||
@@ -24,8 +27,36 @@ function renderTable(devices) {
|
|||||||
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
||||||
${USER_ROLE === 'admin' ? `<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>` : ''}
|
${USER_ROLE === 'admin' ? `<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>` : ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>`;
|
||||||
`).join("");
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStateLabel(state) {
|
||||||
|
return {0: '离线', 1: '在线', 2: '通信不良'}[state] || '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStateClass(state) {
|
||||||
|
return {0: 'status-offline', 1: 'status-online', 2: 'status-poor'}[state] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步刷新所有设备的在线状态
|
||||||
|
async function refreshDeviceStatuses() {
|
||||||
|
const cells = document.querySelectorAll(".status-cell");
|
||||||
|
for (const cell of cells) {
|
||||||
|
const deviceId = cell.dataset.deviceId;
|
||||||
|
if (!deviceId) continue;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/devices/${deviceId}/status`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
cell.textContent = data.state_name;
|
||||||
|
cell.className = getStateClass(data.state) + " status-cell";
|
||||||
|
cell.dataset.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 网络错误静默跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function esc(s) { return s.replace(/'/g, "\\'").replace(/"/g, """); }
|
function esc(s) { return s.replace(/'/g, "\\'").replace(/"/g, """); }
|
||||||
@@ -52,3 +83,5 @@ async function editName(id, currentName, td) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadDevices();
|
loadDevices();
|
||||||
|
// 每 5 秒异步刷新设备在线状态
|
||||||
|
setInterval(refreshDeviceStatuses, 5000);
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ function updateTestModeUI(mode) {
|
|||||||
|
|
||||||
async function loadInitialData() {
|
async function loadInitialData() {
|
||||||
await loadTestMode();
|
await loadTestMode();
|
||||||
|
refreshDeviceStatus();
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/automation/${DNT_ID}/progress`);
|
const resp = await fetch(`/api/automation/${DNT_ID}/progress`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
@@ -301,6 +302,33 @@ async function loadInitialData() {
|
|||||||
}
|
}
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
|
|
||||||
|
// ─── 设备状态异步刷新 ──────────────────────────
|
||||||
|
|
||||||
|
async function refreshDeviceStatus() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/devices/${DNT_ID}/status`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
const el = document.getElementById("device-status-text");
|
||||||
|
if (el) {
|
||||||
|
el.textContent = data.state_name;
|
||||||
|
if (data.state === 1) {
|
||||||
|
el.className = "status-online";
|
||||||
|
} else if (data.state === 2) {
|
||||||
|
el.className = "status-poor";
|
||||||
|
} else {
|
||||||
|
el.className = "status-offline";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每 5 秒刷新设备状态
|
||||||
|
setInterval(refreshDeviceStatus, 5000);
|
||||||
|
|
||||||
// ─── UI ────────────────────────────────────────
|
// ─── UI ────────────────────────────────────────
|
||||||
|
|
||||||
function setStatus(msg) {
|
function setStatus(msg) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
||||||
<a href="/test-data" class="{% if request.path == '/test-data' %}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' %}
|
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||||
|
<a href="/device-logs" class="{% if request.path == '/device-logs' %}active{% endif %}">设备日志</a>
|
||||||
<a href="/logs/" class="{% if request.path == '/logs/' %}active{% endif %}">操作日志</a>
|
<a href="/logs/" class="{% if request.path == '/logs/' %}active{% endif %}">操作日志</a>
|
||||||
<a href="/users/" class="{% if request.path == '/users/' %}active{% endif %}">用户管理</a>
|
<a href="/users/" class="{% if request.path == '/users/' %}active{% endif %}">用户管理</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
141
edc-web/app/templates/device_logs.html
Normal file
141
edc-web/app/templates/device_logs.html
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
{% 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>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button onclick="searchLogs(1)" class="btn-search">查询</button>
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<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;
|
||||||
|
|
||||||
|
async function searchLogs(page = 1) {
|
||||||
|
currentPage = page;
|
||||||
|
const serial = document.getElementById("search-serial").value;
|
||||||
|
const event_type = document.getElementById("search-event").value;
|
||||||
|
const params = new URLSearchParams({page, per_page: 30});
|
||||||
|
if (serial) params.set("serial", serial);
|
||||||
|
if (event_type) params.set("event_type", event_type);
|
||||||
|
|
||||||
|
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;';
|
||||||
|
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: '通信不良'};
|
||||||
|
return m[t] || t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(v) {
|
||||||
|
if (!v) return '-';
|
||||||
|
const d = new Date(v);
|
||||||
|
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, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteLogs() {
|
||||||
|
const serial = document.getElementById("search-serial").value;
|
||||||
|
const event_type = document.getElementById("search-event").value;
|
||||||
|
if (!serial && !event_type) {
|
||||||
|
alert("请至少输入设备序列号或选择事件类型作为删除条件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const msg = `确认删除设备日志?\n条件: serial=${serial || '(无)'} type=${event_type || '(无)'}\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}),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
alert(`已删除 ${data.deleted} 条记录`);
|
||||||
|
searchLogs(1);
|
||||||
|
} else {
|
||||||
|
alert("删除失败: " + (data.error || "未知错误"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchLogs(1);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
<div class="test-header">
|
<div class="test-header">
|
||||||
<a href="/">← 返回设备列表</a>
|
<a href="/">← 返回设备列表</a>
|
||||||
<h2>测试操作 — {{ device.serial }} ({{ device.name or '未命名' }})</h2>
|
<h2>测试操作 — {{ device.serial }} ({{ device.name or '未命名' }})</h2>
|
||||||
|
<div id="device-status-bar" style="margin-top:4px;font-size:14px;">
|
||||||
|
设备状态:<span id="device-status-text" class="{% if device.state == 1 %}status-online{% elif device.state == 2 %}status-poor{% else %}status-offline{% endif %}">加载中…</span>
|
||||||
|
</div>
|
||||||
<div id="test-mode-indicator" style="margin-top:4px;font-size:14px;color:#888;">加载中…</div>
|
<div id="test-mode-indicator" style="margin-top:4px;font-size:14px;color:#888;">加载中…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Submodule edc_server updated: 3a74759066...ef890fafc6
Reference in New Issue
Block a user