feat: admin用户支持删除测试数据(按条件/日期范围),含确认框+日志
This commit is contained in:
@@ -582,3 +582,48 @@ def delete_vehicle_base_test(test_id: int):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
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()
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from flask import Blueprint, jsonify, render_template, request, Response
|
from flask import Blueprint, jsonify, render_template, request, Response
|
||||||
from flask_login import login_required
|
from flask_login import login_required, current_user
|
||||||
from app.models import get_test_data, get_all_test_data_for_export
|
from app.models import get_test_data, get_all_test_data_for_export, delete_test_data, insert_log
|
||||||
|
|
||||||
bp = Blueprint("test_data", __name__)
|
bp = Blueprint("test_data", __name__)
|
||||||
|
|
||||||
@@ -78,3 +78,39 @@ def api_export():
|
|||||||
mimetype="text/csv",
|
mimetype="text/csv",
|
||||||
headers={"Content-Disposition": "attachment; filename=test_data.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})
|
||||||
|
|||||||
@@ -364,3 +364,46 @@ async function loadChart() {
|
|||||||
|
|
||||||
renderHead();
|
renderHead();
|
||||||
searchData(1);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button id="btn-chart" class="btn-chart" onclick="toggleChart()">📈 图表</button>
|
<button id="btn-chart" class="btn-chart" onclick="toggleChart()">📈 图表</button>
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<button id="btn-delete" class="btn-delete" onclick="confirmDelete()">🗑 删除</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="chart-container" style="display:none; width:100%; height:500px; margin-bottom:16px;"></div>
|
<div id="chart-container" style="display:none; width:100%; height:500px; margin-bottom:16px;"></div>
|
||||||
@@ -73,6 +76,17 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.btn-chart.active { background: #27ae60; color: #fff; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||||||
<script src="{{ url_for('static', filename='js/test_data.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/test_data.js') }}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user