From 6a3aaf3c05d700cb808b6845010c29133ef24070 Mon Sep 17 00:00:00 2001 From: wangfq Date: Fri, 5 Jun 2026 13:44:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20admin=E7=94=A8=E6=88=B7=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=88=A0=E9=99=A4=E6=B5=8B=E8=AF=95=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=EF=BC=88=E6=8C=89=E6=9D=A1=E4=BB=B6/=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=EF=BC=89=EF=BC=8C=E5=90=AB=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E6=A1=86+=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- edc-web/app/models.py | 45 ++++++++++++++++++++++++++++ edc-web/app/routes/test_data.py | 40 +++++++++++++++++++++++-- edc-web/app/static/js/test_data.js | 43 ++++++++++++++++++++++++++ edc-web/app/templates/test_data.html | 14 +++++++++ 4 files changed, 140 insertions(+), 2 deletions(-) diff --git a/edc-web/app/models.py b/edc-web/app/models.py index 4ff7130..161e2b2 100644 --- a/edc-web/app/models.py +++ b/edc-web/app/models.py @@ -582,3 +582,48 @@ def delete_vehicle_base_test(test_id: int): conn.commit() finally: conn.close() + + +# ─── 测试数据删除 ────────────────────────────────────────────── + +def delete_test_data(serial: str = "", date_from: str = "", + date_to: str = "", data_source: str = "") -> int: + """删除符合条件的测试数据,返回删除行数 + + 必须至少提供一个条件(serial / date范围 / data_source),不允许无条件全删。 + """ + conn = get_conn() + try: + with conn.cursor() as cur: + where = [] + params = [] + if serial: + where.append("t.dnt_id IN (SELECT id FROM dnt_info WHERE serial LIKE %s)") + params.append(f"%{serial}%") + if date_from: + where.append("t.create_time >= %s") + params.append(date_from) + if date_to: + where.append("t.create_time <= %s") + params.append(date_to + " 23:59:59") + if data_source: + where.append("t.data_source = %s") + params.append(data_source) + + if not where: + return 0 # 拒绝无条件全删 + + where_clause = " AND ".join(where) + cur.execute( + f"SELECT COUNT(*) as cnt FROM tb_state_tst t WHERE {where_clause}", + params, + ) + cnt = cur.fetchone()["cnt"] + + cur.execute( + f"DELETE t FROM tb_state_tst t WHERE {where_clause}", params, + ) + conn.commit() + return cnt + finally: + conn.close() diff --git a/edc-web/app/routes/test_data.py b/edc-web/app/routes/test_data.py index 72c551d..1b640f3 100644 --- a/edc-web/app/routes/test_data.py +++ b/edc-web/app/routes/test_data.py @@ -3,8 +3,8 @@ import csv import io from flask import Blueprint, jsonify, render_template, request, Response -from flask_login import login_required -from app.models import get_test_data, get_all_test_data_for_export +from flask_login import login_required, current_user +from app.models import get_test_data, get_all_test_data_for_export, delete_test_data, insert_log bp = Blueprint("test_data", __name__) @@ -78,3 +78,39 @@ def api_export(): mimetype="text/csv", headers={"Content-Disposition": "attachment; filename=test_data.csv"}, ) + + +@bp.route("/api/test-data/delete", methods=["POST"]) +@login_required +def api_delete(): + """删除测试数据(仅 admin)""" + if current_user.role != "admin": + return jsonify({"ok": False, "error": "无权限"}), 403 + + data = request.get_json() or {} + serial = data.get("serial", "") + date_from = data.get("date_from", "") + date_to = data.get("date_to", "") + data_source = data.get("data_source", "") + + cnt = delete_test_data(serial, date_from, date_to, data_source) + + detail_parts = [f"删除 {cnt} 条测试数据"] + if serial: + detail_parts.append(f"设备={serial}") + if date_from: + detail_parts.append(f"从{date_from}") + if date_to: + detail_parts.append(f"至{date_to}") + if data_source: + detail_parts.append(f"来源={data_source}") + + insert_log( + current_user.id, current_user.username, "delete", + target="test_data", + detail=", ".join(detail_parts), + result="ok", + ip=request.remote_addr or "", + ) + + return jsonify({"ok": True, "deleted": cnt}) diff --git a/edc-web/app/static/js/test_data.js b/edc-web/app/static/js/test_data.js index 7999b2b..39ef66b 100644 --- a/edc-web/app/static/js/test_data.js +++ b/edc-web/app/static/js/test_data.js @@ -364,3 +364,46 @@ async function loadChart() { renderHead(); searchData(1); + +// ─── 删除(admin)───────────────────────────────── + +function confirmDelete() { + const serial = document.getElementById('search-serial').value; + const dateFrom = document.getElementById('search-date-from').value; + const dateTo = document.getElementById('search-date-to').value; + const v = VIEWS[currentView]; + const ds = v.data_source || ''; + + let desc = ''; + if (serial) desc += `设备: ${serial}\n`; + if (dateFrom || dateTo) desc += `日期: ${dateFrom || '不限'} ~ ${dateTo || '不限'}\n`; + if (ds) desc += `数据来源: ${ds}\n`; + if (!desc) desc = '⚠ 未设置任何筛选条件,不会删除任何数据'; + + const msg = `确认删除以下条件的测试数据?\n\n${desc}\n此操作不可撤销!`; + if (!confirm(msg)) return; + + doDelete(serial, dateFrom, dateTo, ds); +} + +async function doDelete(serial, dateFrom, dateTo, dataSource) { + try { + const resp = await fetch('/api/test-data/delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + serial, date_from: dateFrom, + date_to: dateTo, data_source: dataSource, + }), + }); + const data = await resp.json(); + if (data.ok) { + alert(`已删除 ${data.deleted} 条记录`); + searchData(1); + } else { + alert('删除失败: ' + (data.error || '未知错误')); + } + } catch (e) { + alert('删除请求失败: ' + e.message); + } +} diff --git a/edc-web/app/templates/test_data.html b/edc-web/app/templates/test_data.html index f42a065..04a3692 100644 --- a/edc-web/app/templates/test_data.html +++ b/edc-web/app/templates/test_data.html @@ -32,6 +32,9 @@ + {% if current_user.role == 'admin' %} + + {% endif %} @@ -73,6 +76,17 @@ font-size: 13px; } .btn-chart.active { background: #27ae60; color: #fff; } +.btn-delete { + margin-left: 8px; + padding: 6px 14px; + border: 1px solid #e74c3c; + background: #fff; + color: #e74c3c; + cursor: pointer; + border-radius: 4px; + font-size: 13px; +} +.btn-delete:hover { background: #e74c3c; color: #fff; }