feat: 告警/考勤/蓝牙/数据日志页面添加批量删除功能
- 新增 POST /api/alarms/batch-delete、/api/bluetooth/batch-delete、 /api/heartbeats/batch-delete 批量删除端点 (最多500条) - 四个页面表格添加全选复选框和"删除选中"按钮 - 提取通用 toggleAllCheckboxes/updateSelCount/_batchDelete 函数 - 数据日志页面根据当前查询类型自动路由到对应的批量删除API via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -553,6 +553,7 @@
|
||||
<input type="date" id="alarmEndDate" style="width:160px">
|
||||
<button class="btn btn-primary" onclick="loadAlarms()"><i class="fas fa-search"></i> 查询</button>
|
||||
<button class="btn btn-secondary" onclick="loadAlarms()"><i class="fas fa-sync-alt"></i> 刷新</button>
|
||||
<button class="btn" style="background:#b91c1c;color:#fff" id="btnBatchDeleteAlarm" onclick="batchDeleteSelectedAlarms()" disabled><i class="fas fa-trash-alt"></i> 删除选中 (<span id="alarmSelCount">0</span>)</button>
|
||||
</div>
|
||||
<div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden relative">
|
||||
<div id="alarmsLoading" class="loading-overlay" style="display:none"><div class="spinner"></div></div>
|
||||
@@ -560,6 +561,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px"><input type="checkbox" id="alarmSelectAll" onchange="toggleAllCheckboxes('alarm-sel-cb','alarmSelCount','btnBatchDeleteAlarm',this.checked)" title="全选"></th>
|
||||
<th>设备ID</th>
|
||||
<th>类型</th>
|
||||
<th>来源</th>
|
||||
@@ -572,7 +574,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="alarmsTableBody">
|
||||
<tr><td colspan="9" class="text-center text-gray-500 py-8">加载中...</td></tr>
|
||||
<tr><td colspan="10" class="text-center text-gray-500 py-8">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -635,6 +637,7 @@
|
||||
<input type="date" id="attEndDate" style="width:160px">
|
||||
<button class="btn btn-primary" onclick="loadAttendance()"><i class="fas fa-search"></i> 查询</button>
|
||||
<button class="btn btn-secondary" onclick="loadAttendance()"><i class="fas fa-sync-alt"></i> 刷新</button>
|
||||
<button class="btn" style="background:#b91c1c;color:#fff" id="btnBatchDeleteAtt" onclick="batchDeleteSelectedAttendance()" disabled><i class="fas fa-trash-alt"></i> 删除选中 (<span id="attSelCount">0</span>)</button>
|
||||
</div>
|
||||
<div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden relative">
|
||||
<div id="attendanceLoading" class="loading-overlay" style="display:none"><div class="spinner"></div></div>
|
||||
@@ -642,6 +645,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px"><input type="checkbox" id="attSelectAll" onchange="toggleAllCheckboxes('att-sel-cb','attSelCount','btnBatchDeleteAtt',this.checked)" title="全选"></th>
|
||||
<th>设备ID</th>
|
||||
<th>类型</th>
|
||||
<th>来源</th>
|
||||
@@ -653,7 +657,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="attendanceTableBody">
|
||||
<tr><td colspan="8" class="text-center text-gray-500 py-8">加载中...</td></tr>
|
||||
<tr><td colspan="9" class="text-center text-gray-500 py-8">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -701,6 +705,7 @@
|
||||
<input type="date" id="btEndDate" style="width:160px">
|
||||
<button class="btn btn-primary" onclick="loadBluetooth()"><i class="fas fa-search"></i> 查询</button>
|
||||
<button class="btn btn-secondary" onclick="loadBluetooth()"><i class="fas fa-sync-alt"></i> 刷新</button>
|
||||
<button class="btn" style="background:#b91c1c;color:#fff" id="btnBatchDeleteBt" onclick="batchDeleteSelectedBluetooth()" disabled><i class="fas fa-trash-alt"></i> 删除选中 (<span id="btSelCount">0</span>)</button>
|
||||
</div>
|
||||
<div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden relative">
|
||||
<div id="bluetoothLoading" class="loading-overlay" style="display:none"><div class="spinner"></div></div>
|
||||
@@ -708,6 +713,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px"><input type="checkbox" id="btSelectAll" onchange="toggleAllCheckboxes('bt-sel-cb','btSelCount','btnBatchDeleteBt',this.checked)" title="全选"></th>
|
||||
<th>设备ID</th>
|
||||
<th>类型</th>
|
||||
<th>信标MAC</th>
|
||||
@@ -719,7 +725,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bluetoothTableBody">
|
||||
<tr><td colspan="8" class="text-center text-gray-500 py-8">加载中...</td></tr>
|
||||
<tr><td colspan="9" class="text-center text-gray-500 py-8">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -906,6 +912,7 @@
|
||||
<input type="date" id="logEndDate" style="width:150px">
|
||||
<button class="btn btn-primary" onclick="loadDataLog()"><i class="fas fa-search"></i> 查询</button>
|
||||
<button class="btn btn-secondary" onclick="loadDataLog()"><i class="fas fa-sync-alt"></i> 刷新</button>
|
||||
<button class="btn" style="background:#b91c1c;color:#fff" id="btnBatchDeleteLog" onclick="batchDeleteSelectedDatalog()" disabled><i class="fas fa-trash-alt"></i> 删除选中 (<span id="logSelCount">0</span>)</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-5 gap-3 mb-4">
|
||||
<div class="stat-card" style="padding:14px;text-align:center;cursor:pointer" onclick="document.getElementById('logTypeFilter').value='location';loadDataLog()">
|
||||
@@ -935,6 +942,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px"><input type="checkbox" id="logSelectAll" onchange="toggleAllCheckboxes('log-sel-cb','logSelCount','btnBatchDeleteLog',this.checked)" title="全选"></th>
|
||||
<th>ID</th>
|
||||
<th>类型</th>
|
||||
<th>设备ID</th>
|
||||
@@ -946,7 +954,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="datalogTableBody">
|
||||
<tr><td colspan="8" class="text-center text-gray-500 py-8">选择筛选条件后点击查询</td></tr>
|
||||
<tr><td colspan="9" class="text-center text-gray-500 py-8">选择筛选条件后点击查询</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1902,32 +1910,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllLocCheckboxes(checked) {
|
||||
document.querySelectorAll('.loc-sel-cb').forEach(cb => { cb.checked = checked; });
|
||||
updateLocSelCount();
|
||||
// ==================== GENERIC BATCH DELETE HELPERS ====================
|
||||
function toggleAllCheckboxes(cbClass, countSpanId, btnId, checked) {
|
||||
document.querySelectorAll('.' + cbClass).forEach(cb => { cb.checked = checked; });
|
||||
updateSelCount(cbClass, countSpanId, btnId);
|
||||
}
|
||||
|
||||
function updateLocSelCount() {
|
||||
const count = document.querySelectorAll('.loc-sel-cb:checked').length;
|
||||
document.getElementById('locSelCount').textContent = count;
|
||||
document.getElementById('btnBatchDeleteLoc').disabled = count === 0;
|
||||
function updateSelCount(cbClass, countSpanId, btnId) {
|
||||
const count = document.querySelectorAll('.' + cbClass + ':checked').length;
|
||||
document.getElementById(countSpanId).textContent = count;
|
||||
document.getElementById(btnId).disabled = count === 0;
|
||||
}
|
||||
// Location (compat wrappers)
|
||||
function toggleAllLocCheckboxes(checked) { toggleAllCheckboxes('loc-sel-cb','locSelCount','btnBatchDeleteLoc', checked); }
|
||||
function updateLocSelCount() { updateSelCount('loc-sel-cb','locSelCount','btnBatchDeleteLoc'); }
|
||||
|
||||
async function batchDeleteSelectedLocations() {
|
||||
const ids = Array.from(document.querySelectorAll('.loc-sel-cb:checked')).map(cb => parseInt(cb.value));
|
||||
async function _batchDelete(cbClass, apiPath, idKey, label, reloadFn) {
|
||||
const ids = Array.from(document.querySelectorAll('.' + cbClass + ':checked')).map(cb => parseInt(cb.value));
|
||||
if (ids.length === 0) { showToast('请先勾选要删除的记录', 'info'); return; }
|
||||
if (!confirm(`确定批量删除选中的 ${ids.length} 条位置记录?`)) return;
|
||||
if (!confirm(`确定批量删除选中的 ${ids.length} 条${label}?`)) return;
|
||||
try {
|
||||
const result = await apiCall(`${API_BASE}/locations/batch-delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ location_ids: ids }),
|
||||
const result = await apiCall(`${API_BASE}/${apiPath}`, {
|
||||
method: 'POST', body: JSON.stringify({ [idKey]: ids }),
|
||||
});
|
||||
showToast(`已删除 ${result.deleted} 条记录`);
|
||||
loadLocationRecords();
|
||||
reloadFn();
|
||||
} catch (err) {
|
||||
showToast('批量删除失败: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
function batchDeleteSelectedLocations() { _batchDelete('loc-sel-cb', 'locations/batch-delete', 'location_ids', '位置记录', loadLocationRecords); }
|
||||
function batchDeleteSelectedAlarms() { _batchDelete('alarm-sel-cb', 'alarms/batch-delete', 'alarm_ids', '告警记录', loadAlarms); }
|
||||
function batchDeleteSelectedAttendance() { _batchDelete('att-sel-cb', 'attendance/batch-delete', 'attendance_ids', '考勤记录', loadAttendance); }
|
||||
function batchDeleteSelectedBluetooth() { _batchDelete('bt-sel-cb', 'bluetooth/batch-delete', 'record_ids', '蓝牙记录', loadBluetooth); }
|
||||
function batchDeleteSelectedDatalog() {
|
||||
const logType = document.getElementById('logTypeFilter').value || 'location';
|
||||
const apiMap = { location: ['locations/batch-delete','location_ids'], alarm: ['alarms/batch-delete','alarm_ids'], attendance: ['attendance/batch-delete','attendance_ids'], bluetooth: ['bluetooth/batch-delete','record_ids'], heartbeat: ['heartbeats/batch-delete','record_ids'] };
|
||||
const [path, key] = apiMap[logType] || apiMap.location;
|
||||
_batchDelete('log-sel-cb', path, key, '记录', loadDataLog);
|
||||
}
|
||||
|
||||
async function batchDeleteNoCoordLocations() {
|
||||
const deviceId = document.getElementById('locDeviceSelect').value || null;
|
||||
@@ -2608,10 +2628,11 @@
|
||||
const tbody = document.getElementById('alarmsTableBody');
|
||||
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-500 py-8">没有告警记录</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-gray-500 py-8">没有告警记录</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(a => `
|
||||
<tr>
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="alarm-sel-cb" value="${a.id}" onchange="updateSelCount('alarm-sel-cb','alarmSelCount','btnBatchDeleteAlarm')"></td>
|
||||
<td class="font-mono text-xs">${escapeHtml(a.device_id || '-')}</td>
|
||||
<td><span class="${alarmTypeClass(a.alarm_type)} font-semibold">${alarmTypeName(a.alarm_type)}</span></td>
|
||||
<td>${escapeHtml(a.alarm_source || '-')}</td>
|
||||
@@ -2629,10 +2650,11 @@
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
document.getElementById('alarmSelectAll').checked = false;
|
||||
renderPagination('alarmsPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadAlarms');
|
||||
} catch (err) {
|
||||
showToast('加载告警失败: ' + err.message, 'error');
|
||||
document.getElementById('alarmsTableBody').innerHTML = '<tr><td colspan="9" class="text-center text-red-400 py-8">加载失败</td></tr>';
|
||||
document.getElementById('alarmsTableBody').innerHTML = '<tr><td colspan="10" class="text-center text-red-400 py-8">加载失败</td></tr>';
|
||||
} finally {
|
||||
hideLoading('alarmsLoading');
|
||||
}
|
||||
@@ -2698,7 +2720,7 @@
|
||||
const tbody = document.getElementById('attendanceTableBody');
|
||||
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-500 py-8">没有考勤记录</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-500 py-8">没有考勤记录</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(a => {
|
||||
const posStr = a.address || (a.latitude != null ? `${Number(a.latitude).toFixed(6)}, ${Number(a.longitude).toFixed(6)}` : '-');
|
||||
@@ -2709,6 +2731,7 @@
|
||||
const srcLabel = {'device':'<i class="fas fa-mobile-alt"></i> 设备','bluetooth':'<i class="fab fa-bluetooth-b"></i> 蓝牙','fence':'<i class="fas fa-draw-polygon"></i> 围栏'}[a.attendance_source] || a.attendance_source || '设备';
|
||||
const srcColor = {'device':'#9ca3af','bluetooth':'#818cf8','fence':'#34d399'}[a.attendance_source] || '#9ca3af';
|
||||
return `<tr>
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="att-sel-cb" value="${a.id}" onchange="updateSelCount('att-sel-cb','attSelCount','btnBatchDeleteAtt')"></td>
|
||||
<td class="font-mono text-xs">${escapeHtml(a.device_id || '-')}</td>
|
||||
<td><span class="${a.attendance_type === 'clock_in' ? 'text-green-400' : 'text-blue-400'} font-semibold">${attendanceTypeName(a.attendance_type)}</span></td>
|
||||
<td style="color:${srcColor};font-size:12px">${srcLabel}</td>
|
||||
@@ -2720,10 +2743,11 @@
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
document.getElementById('attSelectAll').checked = false;
|
||||
renderPagination('attendancePagination', data.total || 0, data.page || p, data.page_size || ps, 'loadAttendance');
|
||||
} catch (err) {
|
||||
showToast('加载考勤记录失败: ' + err.message, 'error');
|
||||
document.getElementById('attendanceTableBody').innerHTML = '<tr><td colspan="8" class="text-center text-red-400 py-8">加载失败</td></tr>';
|
||||
document.getElementById('attendanceTableBody').innerHTML = '<tr><td colspan="9" class="text-center text-red-400 py-8">加载失败</td></tr>';
|
||||
} finally {
|
||||
hideLoading('attendanceLoading');
|
||||
}
|
||||
@@ -2764,7 +2788,7 @@
|
||||
const tbody = document.getElementById('bluetoothTableBody');
|
||||
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-500 py-8">没有蓝牙记录</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-500 py-8">没有蓝牙记录</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(b => {
|
||||
const typeIcon = b.record_type === 'punch' ? '<i class="fas fa-fingerprint text-purple-400"></i>' : '<i class="fas fa-map-marker-alt text-cyan-400"></i>';
|
||||
@@ -2777,6 +2801,7 @@
|
||||
const battStr = b.beacon_battery != null ? `${Number(b.beacon_battery).toFixed(2)}${b.beacon_battery_unit || 'V'}` : '-';
|
||||
const attStr = b.attendance_type ? `<span class="${b.attendance_type === 'clock_in' ? 'text-green-400' : 'text-blue-400'}">${attendanceTypeName(b.attendance_type)}</span>` : '-';
|
||||
return `<tr>
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="bt-sel-cb" value="${b.id}" onchange="updateSelCount('bt-sel-cb','btSelCount','btnBatchDeleteBt')"></td>
|
||||
<td class="font-mono text-xs">${escapeHtml(b.device_id || '-')}</td>
|
||||
<td>${typeIcon} <span class="font-semibold">${typeName}</span></td>
|
||||
<td class="font-mono text-xs">${escapeHtml(mac)}</td>
|
||||
@@ -2788,10 +2813,11 @@
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
document.getElementById('btSelectAll').checked = false;
|
||||
renderPagination('bluetoothPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadBluetooth');
|
||||
} catch (err) {
|
||||
showToast('加载蓝牙记录失败: ' + err.message, 'error');
|
||||
document.getElementById('bluetoothTableBody').innerHTML = '<tr><td colspan="8" class="text-center text-red-400 py-8">加载失败</td></tr>';
|
||||
document.getElementById('bluetoothTableBody').innerHTML = '<tr><td colspan="9" class="text-center text-red-400 py-8">加载失败</td></tr>';
|
||||
} finally {
|
||||
hideLoading('bluetoothLoading');
|
||||
}
|
||||
@@ -2919,7 +2945,7 @@
|
||||
const tbody = document.getElementById('datalogTableBody');
|
||||
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-500 py-8">暂无记录</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-500 py-8">暂无记录</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(r => {
|
||||
const detail = _logDetail(type, r);
|
||||
@@ -2930,6 +2956,7 @@
|
||||
const time = r.recorded_at || r.alarm_time || r.heartbeat_time || r.created_at;
|
||||
const typeBadge = _logTypeBadge(type);
|
||||
return `<tr>
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="log-sel-cb" value="${r.id}" onchange="updateSelCount('log-sel-cb','logSelCount','btnBatchDeleteLog')"></td>
|
||||
<td class="font-mono text-xs">${r.id}</td>
|
||||
<td>${typeBadge}</td>
|
||||
<td>${r.device_id || '-'}</td>
|
||||
@@ -2941,9 +2968,10 @@
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
document.getElementById('logSelectAll').checked = false;
|
||||
renderPagination('datalogPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadDataLog');
|
||||
} catch (err) {
|
||||
document.getElementById('datalogTableBody').innerHTML = '<tr><td colspan="8" class="text-center text-red-400 py-8">加载失败: ' + escapeHtml(err.message) + '</td></tr>';
|
||||
document.getElementById('datalogTableBody').innerHTML = '<tr><td colspan="9" class="text-center text-red-400 py-8">加载失败: ' + escapeHtml(err.message) + '</td></tr>';
|
||||
} finally {
|
||||
hideLoading('datalogLoading');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user