feat: 设备日志增加时间范围查询 + CSV 导出
- 查询 API 增加 date_from/date_to 参数(前端日期+时间选择器) - 新增 /api/device-logs/export CSV 导出端点 - 新增 export_device_logs() 模型函数(全量不分页) - 删除校验放宽:允许纯时间范围作为删除条件 - 前端增加导出 CSV 按钮,遵循 test_data 页面模式
This commit is contained in:
@@ -832,7 +832,8 @@ def delete_test_data(serial: str = "", date_from: str = "",
|
|||||||
# ─── tb_device_log ─────────────────────────────────────────────────
|
# ─── tb_device_log ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_device_logs(page: int = 1, per_page: int = 30,
|
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)"""
|
"""分页查询设备事件日志,返回 (records, total)"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
@@ -845,6 +846,12 @@ def get_device_logs(page: int = 1, per_page: int = 30,
|
|||||||
if event_type:
|
if event_type:
|
||||||
where.append("event_type = %s")
|
where.append("event_type = %s")
|
||||||
params.append(event_type)
|
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"
|
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()
|
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 = "",
|
def delete_device_logs(serial: str = "", event_type: str = "",
|
||||||
date_from: str = "", date_to: str = "") -> int:
|
date_from: str = "", date_to: str = "") -> int:
|
||||||
"""删除符合条件的设备日志,返回删除行数。至少需要一个条件。"""
|
"""删除符合条件的设备日志,返回删除行数。至少需要一个条件。"""
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
"""设备事件日志 API"""
|
"""设备事件日志 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 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__)
|
bp = Blueprint("device_logs", __name__)
|
||||||
|
|
||||||
@@ -22,15 +25,49 @@ def api_device_logs():
|
|||||||
per_page = request.args.get("per_page", 30, type=int)
|
per_page = request.args.get("per_page", 30, type=int)
|
||||||
serial = request.args.get("serial", "", type=str)
|
serial = request.args.get("serial", "", type=str)
|
||||||
event_type = request.args.get("event_type", "", 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(
|
records, total = get_device_logs(
|
||||||
page=page, per_page=per_page,
|
page=page, per_page=per_page,
|
||||||
serial=serial, event_type=event_type,
|
serial=serial, event_type=event_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
)
|
)
|
||||||
pages = max(1, (total + per_page - 1) // per_page)
|
pages = max(1, (total + per_page - 1) // per_page)
|
||||||
return jsonify({"records": records, "total": total, "pages": pages})
|
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"])
|
@bp.route("/api/device-logs/delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_device_logs_delete():
|
def api_device_logs_delete():
|
||||||
|
|||||||
@@ -17,7 +17,16 @@
|
|||||||
<option value="tcp_disconnect">TCP断开</option>
|
<option value="tcp_disconnect">TCP断开</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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="searchLogs(1)" class="btn-search">查询</button>
|
||||||
|
<button onclick="exportCSV()" class="btn-export">导出 CSV</button>
|
||||||
{% if current_user.role == 'admin' %}
|
{% if current_user.role == 'admin' %}
|
||||||
<button onclick="confirmDeleteLogs()" class="btn-delete">🗑 删除</button>
|
<button onclick="confirmDeleteLogs()" class="btn-delete">🗑 删除</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -44,13 +53,25 @@
|
|||||||
<script>
|
<script>
|
||||||
let currentPage = 1, totalPages = 1;
|
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) {
|
async function searchLogs(page = 1) {
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
const serial = document.getElementById("search-serial").value;
|
const serial = document.getElementById("search-serial").value;
|
||||||
const event_type = document.getElementById("search-event").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});
|
const params = new URLSearchParams({page, per_page: 30});
|
||||||
if (serial) params.set("serial", serial);
|
if (serial) params.set("serial", serial);
|
||||||
if (event_type) params.set("event_type", event_type);
|
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 resp = await fetch(`/api/device-logs?${params}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
@@ -120,20 +141,41 @@ function escHtml(s) {
|
|||||||
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
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() {
|
async function confirmDeleteLogs() {
|
||||||
const serial = document.getElementById("search-serial").value;
|
const serial = document.getElementById("search-serial").value;
|
||||||
const event_type = document.getElementById("search-event").value;
|
const event_type = document.getElementById("search-event").value;
|
||||||
if (!serial && !event_type) {
|
const date_from = getDatetime("search-date-from", "search-time-from");
|
||||||
alert("请至少输入设备序列号或选择事件类型作为删除条件");
|
const date_to = getDatetime("search-date-to", "search-time-to");
|
||||||
|
if (!serial && !event_type && !date_from && !date_to) {
|
||||||
|
alert("请至少输入设备序列号、选择事件类型或指定时间范围作为删除条件");
|
||||||
return;
|
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;
|
if (!confirm(msg)) return;
|
||||||
|
|
||||||
const resp = await fetch("/api/device-logs/delete", {
|
const resp = await fetch("/api/device-logs/delete", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {"Content-Type": "application/json"},
|
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();
|
const data = await resp.json();
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user