From ee136cc7074d3788bdbbd6683658822d05526501 Mon Sep 17 00:00:00 2001 From: wangfq Date: Wed, 10 Jun 2026 09:14:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20edc-web=20=E8=AE=BE=E5=A4=87=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=AE=A1=E7=90=86=E9=A1=B5=20+=20=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=AE=9E=E6=97=B6=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /device-logs 设备事件日志管理页 (admin 权限) - 支持按设备序列号/事件类型筛选查询 - 支持 admin 按条件删除日志 - 不同事件类型彩色标识 (在线=绿, 离线=红, 通信不良=橙) - 新增 /api/devices//status 设备状态 API - 设备列表页:每 5s 异步刷新所有设备在线状态 - 测试操作页:顶部显示设备状态,每 5s 异步刷新 - dnt_info state 支持三态显示 (在线/离线/通信不良) - 导航栏增加「设备日志」入口 (admin only) --- edc-web/app/__init__.py | 2 + edc-web/app/models.py | 76 +++++++++++++ edc-web/app/routes/device_logs.py | 58 ++++++++++ edc-web/app/routes/devices.py | 18 +++- edc-web/app/static/css/style.css | 1 + edc-web/app/static/js/devices.js | 43 +++++++- edc-web/app/static/js/test_op.js | 28 +++++ edc-web/app/templates/base.html | 1 + edc-web/app/templates/device_logs.html | 141 +++++++++++++++++++++++++ edc-web/app/templates/test_op.html | 3 + edc_server | 2 +- 11 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 edc-web/app/routes/device_logs.py create mode 100644 edc-web/app/templates/device_logs.html diff --git a/edc-web/app/__init__.py b/edc-web/app/__init__.py index 49eb4a6..b2c9e43 100644 --- a/edc-web/app/__init__.py +++ b/edc-web/app/__init__.py @@ -20,6 +20,7 @@ def create_app() -> Flask: from app.routes.fixture import bp as fixture_bp from app.routes.users import bp as users_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(test_op_bp) @@ -27,6 +28,7 @@ def create_app() -> Flask: app.register_blueprint(fixture_bp) app.register_blueprint(users_bp) app.register_blueprint(logs_bp) + app.register_blueprint(device_logs_bp) # 初始化默认管理员 _ensure_admin() diff --git a/edc-web/app/models.py b/edc-web/app/models.py index 9c9bf22..511ebca 100644 --- a/edc-web/app/models.py +++ b/edc-web/app/models.py @@ -827,3 +827,79 @@ def delete_test_data(serial: str = "", date_from: str = "", return cnt finally: 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() diff --git a/edc-web/app/routes/device_logs.py b/edc-web/app/routes/device_logs.py new file mode 100644 index 0000000..20b1f75 --- /dev/null +++ b/edc-web/app/routes/device_logs.py @@ -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}) diff --git a/edc-web/app/routes/devices.py b/edc-web/app/routes/devices.py index 1c3a611..55f4ddc 100644 --- a/edc-web/app/routes/devices.py +++ b/edc-web/app/routes/devices.py @@ -2,7 +2,7 @@ from flask import Blueprint, jsonify, render_template, request 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__) @@ -21,6 +21,22 @@ def api_devices(): return jsonify(devices) +@bp.route("/api/devices//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//name", methods=["PUT"]) def api_update_name(device_id): """修改设备名称""" diff --git a/edc-web/app/static/css/style.css b/edc-web/app/static/css/style.css index 2f234d3..e1a7232 100644 --- a/edc-web/app/static/css/style.css +++ b/edc-web/app/static/css/style.css @@ -23,6 +23,7 @@ tr:hover { background: #f8f9fa; } /* === Online Status === */ .status-online { color: #27ae60; font-weight: 600; } .status-offline { color: #bdc3c7; } +.status-poor { color: #f39c12; font-weight: 600; } /* === Editable Name === */ .editable-name { cursor: pointer; border-bottom: 1px dashed transparent; } diff --git a/edc-web/app/static/js/devices.js b/edc-web/app/static/js/devices.js index 29606ae..2b76e80 100644 --- a/edc-web/app/static/js/devices.js +++ b/edc-web/app/static/js/devices.js @@ -8,15 +8,18 @@ async function loadDevices() { function renderTable(devices) { 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 ` ${d.serial} ${d.name || '(点击编辑)'} ${d.ip || '-'} - - ${d.state === 1 ? '在线' : '离线'} + + ${stateLabel} ${d.version || '-'} ${d.last_login || '-'} @@ -24,8 +27,36 @@ function renderTable(devices) { ${USER_ROLE === 'admin' ? `` : ''} - - `).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, """); } @@ -52,3 +83,5 @@ async function editName(id, currentName, td) { } loadDevices(); +// 每 5 秒异步刷新设备在线状态 +setInterval(refreshDeviceStatuses, 5000); diff --git a/edc-web/app/static/js/test_op.js b/edc-web/app/static/js/test_op.js index 5a90659..6001a91 100644 --- a/edc-web/app/static/js/test_op.js +++ b/edc-web/app/static/js/test_op.js @@ -290,6 +290,7 @@ function updateTestModeUI(mode) { async function loadInitialData() { await loadTestMode(); + refreshDeviceStatus(); try { const resp = await fetch(`/api/automation/${DNT_ID}/progress`); const data = await resp.json(); @@ -301,6 +302,33 @@ async function 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 ──────────────────────────────────────── function setStatus(msg) { diff --git a/edc-web/app/templates/base.html b/edc-web/app/templates/base.html index df2735a..b4a3220 100644 --- a/edc-web/app/templates/base.html +++ b/edc-web/app/templates/base.html @@ -11,6 +11,7 @@ 设备 测试信息 {% if current_user.is_authenticated and current_user.role == 'admin' %} + 设备日志 操作日志 用户管理 {% endif %} diff --git a/edc-web/app/templates/device_logs.html b/edc-web/app/templates/device_logs.html new file mode 100644 index 0000000..405abd5 --- /dev/null +++ b/edc-web/app/templates/device_logs.html @@ -0,0 +1,141 @@ +{% extends "base.html" %} +{% block title %}设备日志 - EDC 工装管理系统{% endblock %} + +{% block content %} +

设备事件日志

+ + + + + + + + + + + + + + + +
ID设备序列号设备IP事件类型事件内容时间
+ + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/edc-web/app/templates/test_op.html b/edc-web/app/templates/test_op.html index e70433b..3ffcffe 100644 --- a/edc-web/app/templates/test_op.html +++ b/edc-web/app/templates/test_op.html @@ -6,6 +6,9 @@
← 返回设备列表

测试操作 — {{ device.serial }} ({{ device.name or '未命名' }})

+
+ 设备状态:加载中… +
加载中…
diff --git a/edc_server b/edc_server index 3a74759..ef890fa 160000 --- a/edc_server +++ b/edc_server @@ -1 +1 @@ -Subproject commit 3a747590663079769471ef1f61e7e650660d562b +Subproject commit ef890fafc6a9daac4a334f9ed8014d8b4195c0f6