feat: 设备日志增加时间范围查询 + CSV 导出

- 查询 API 增加 date_from/date_to 参数(前端日期+时间选择器)
- 新增 /api/device-logs/export CSV 导出端点
- 新增 export_device_logs() 模型函数(全量不分页)
- 删除校验放宽:允许纯时间范围作为删除条件
- 前端增加导出 CSV 按钮,遵循 test_data 页面模式
This commit is contained in:
wangfq
2026-06-10 10:44:19 +08:00
parent 6b35d07025
commit d3b6d79a03
3 changed files with 125 additions and 7 deletions

View File

@@ -832,7 +832,8 @@ def delete_test_data(serial: str = "", date_from: str = "",
# ─── tb_device_log ─────────────────────────────────────────────────
def get_device_logs(page: int = 1, per_page: int = 30,
serial: str = "", event_type: str = "") -> tuple[list[dict], int]:
serial: str = "", event_type: str = "",
date_from: str = "", date_to: str = "") -> tuple[list[dict], int]:
"""分页查询设备事件日志,返回 (records, total)"""
conn = get_conn()
try:
@@ -845,6 +846,12 @@ def get_device_logs(page: int = 1, per_page: int = 30,
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")
where_clause = " AND ".join(where) if where else "1=1"
@@ -865,6 +872,38 @@ def get_device_logs(page: int = 1, per_page: int = 30,
conn.close()
def export_device_logs(serial: str = "", event_type: str = "",
date_from: str = "", date_to: str = "") -> list[dict]:
"""导出全部设备事件日志(不分页)"""
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")
where_clause = " AND ".join(where) if where else "1=1"
cur.execute(
f"SELECT * FROM tb_device_log WHERE {where_clause} "
f"ORDER BY id DESC",
params,
)
return cur.fetchall()
finally:
conn.close()
def delete_device_logs(serial: str = "", event_type: str = "",
date_from: str = "", date_to: str = "") -> int:
"""删除符合条件的设备日志,返回删除行数。至少需要一个条件。"""

View File

@@ -1,8 +1,11 @@
"""设备事件日志 API"""
from flask import Blueprint, jsonify, render_template, request
import csv
import io
from flask import Blueprint, jsonify, render_template, request, Response
from flask_login import login_required, current_user
from app.models import get_device_logs, delete_device_logs, insert_log
from app.models import get_device_logs, export_device_logs, delete_device_logs, insert_log
bp = Blueprint("device_logs", __name__)
@@ -22,15 +25,49 @@ def api_device_logs():
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)
date_from = request.args.get("date_from", "", type=str)
date_to = request.args.get("date_to", "", type=str)
records, total = get_device_logs(
page=page, per_page=per_page,
serial=serial, event_type=event_type,
date_from=date_from, date_to=date_to,
)
pages = max(1, (total + per_page - 1) // per_page)
return jsonify({"records": records, "total": total, "pages": pages})
@bp.route("/api/device-logs/export")
@login_required
def api_export():
"""导出设备事件日志为 CSV"""
serial = request.args.get("serial", "", type=str)
event_type = request.args.get("event_type", "", type=str)
date_from = request.args.get("date_from", "", type=str)
date_to = request.args.get("date_to", "", type=str)
records = export_device_logs(
serial=serial, event_type=event_type,
date_from=date_from, date_to=date_to,
)
output = io.StringIO()
writer = csv.writer(output)
if records:
headers = list(records[0].keys())
writer.writerow(headers)
for r in records:
writer.writerow(r.values())
output.seek(0)
return Response(
output.getvalue(),
mimetype="text/csv",
headers={"Content-Disposition": "attachment; filename=device_logs.csv"},
)
@bp.route("/api/device-logs/delete", methods=["POST"])
@login_required
def api_device_logs_delete():

View File

@@ -17,7 +17,16 @@
<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 == 'admin' %}
<button onclick="confirmDeleteLogs()" class="btn-delete">🗑 删除</button>
{% endif %}
@@ -44,13 +53,25 @@
<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();
@@ -120,20 +141,41 @@ function escHtml(s) {
return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ─── 导出 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;
if (!serial && !event_type) {
alert("请至少输入设备序列号或选择事件类型作为删除条件");
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 || '(无)'}\n此操作不可撤销!`;
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}),
body: JSON.stringify({serial, event_type, date_from, date_to}),
});
const data = await resp.json();
if (data.ok) {