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:
2026-03-31 02:03:21 +00:00
parent a97dcd07a5
commit 61c300bad8
4 changed files with 131 additions and 27 deletions

View File

@@ -131,6 +131,32 @@ async def alarm_stats(db: AsyncSession = Depends(get_db)):
) )
@router.post(
"/batch-delete",
response_model=APIResponse[dict],
summary="批量删除告警记录 / Batch delete alarms",
dependencies=[Depends(require_write)],
)
async def batch_delete_alarms(
body: dict,
db: AsyncSession = Depends(get_db),
):
"""批量删除告警记录最多500条。 / Batch delete alarm records (max 500)."""
alarm_ids = body.get("alarm_ids", [])
if not alarm_ids:
raise HTTPException(status_code=400, detail="alarm_ids is required")
if len(alarm_ids) > 500:
raise HTTPException(status_code=400, detail="Max 500 records per request")
result = await db.execute(
select(AlarmRecord).where(AlarmRecord.id.in_(alarm_ids))
)
records = list(result.scalars().all())
for r in records:
await db.delete(r)
await db.flush()
return APIResponse(data={"deleted": len(records)})
@router.get( @router.get(
"/{alarm_id}", "/{alarm_id}",
response_model=APIResponse[AlarmRecordResponse], response_model=APIResponse[AlarmRecordResponse],

View File

@@ -142,6 +142,31 @@ async def device_bluetooth_records(
) )
@router.post(
"/batch-delete",
response_model=APIResponse[dict],
summary="批量删除蓝牙记录 / Batch delete bluetooth records",
)
async def batch_delete_bluetooth(
body: dict,
db: AsyncSession = Depends(get_db),
):
"""批量删除蓝牙记录最多500条。 / Batch delete bluetooth records (max 500)."""
record_ids = body.get("record_ids", [])
if not record_ids:
raise HTTPException(status_code=400, detail="record_ids is required")
if len(record_ids) > 500:
raise HTTPException(status_code=400, detail="Max 500 records per request")
result = await db.execute(
select(BluetoothRecord).where(BluetoothRecord.id.in_(record_ids))
)
records = list(result.scalars().all())
for r in records:
await db.delete(r)
await db.flush()
return APIResponse(data={"deleted": len(records)})
# NOTE: /{record_id} must be after /device/{device_id} to avoid route conflicts # NOTE: /{record_id} must be after /device/{device_id} to avoid route conflicts
@router.get( @router.get(
"/{record_id}", "/{record_id}",

View File

@@ -73,6 +73,31 @@ async def list_heartbeats(
) )
@router.post(
"/batch-delete",
response_model=APIResponse[dict],
summary="批量删除心跳记录 / Batch delete heartbeats",
)
async def batch_delete_heartbeats(
body: dict,
db: AsyncSession = Depends(get_db),
):
"""批量删除心跳记录最多500条。 / Batch delete heartbeat records (max 500)."""
record_ids = body.get("record_ids", [])
if not record_ids:
raise HTTPException(status_code=400, detail="record_ids is required")
if len(record_ids) > 500:
raise HTTPException(status_code=400, detail="Max 500 records per request")
result = await db.execute(
select(HeartbeatRecord).where(HeartbeatRecord.id.in_(record_ids))
)
records = list(result.scalars().all())
for r in records:
await db.delete(r)
await db.flush()
return APIResponse(data={"deleted": len(records)})
@router.get( @router.get(
"/{heartbeat_id}", "/{heartbeat_id}",
response_model=APIResponse[HeartbeatRecordResponse], response_model=APIResponse[HeartbeatRecordResponse],

View File

@@ -553,6 +553,7 @@
<input type="date" id="alarmEndDate" style="width:160px"> <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-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 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>
<div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden relative"> <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> <div id="alarmsLoading" class="loading-overlay" style="display:none"><div class="spinner"></div></div>
@@ -560,6 +561,7 @@
<table> <table>
<thead> <thead>
<tr> <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>设备ID</th>
<th>类型</th> <th>类型</th>
<th>来源</th> <th>来源</th>
@@ -572,7 +574,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="alarmsTableBody"> <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> </tbody>
</table> </table>
</div> </div>
@@ -635,6 +637,7 @@
<input type="date" id="attEndDate" style="width:160px"> <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-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 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>
<div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden relative"> <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> <div id="attendanceLoading" class="loading-overlay" style="display:none"><div class="spinner"></div></div>
@@ -642,6 +645,7 @@
<table> <table>
<thead> <thead>
<tr> <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>设备ID</th>
<th>类型</th> <th>类型</th>
<th>来源</th> <th>来源</th>
@@ -653,7 +657,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="attendanceTableBody"> <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> </tbody>
</table> </table>
</div> </div>
@@ -701,6 +705,7 @@
<input type="date" id="btEndDate" style="width:160px"> <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-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 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>
<div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden relative"> <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> <div id="bluetoothLoading" class="loading-overlay" style="display:none"><div class="spinner"></div></div>
@@ -708,6 +713,7 @@
<table> <table>
<thead> <thead>
<tr> <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>设备ID</th>
<th>类型</th> <th>类型</th>
<th>信标MAC</th> <th>信标MAC</th>
@@ -719,7 +725,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="bluetoothTableBody"> <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> </tbody>
</table> </table>
</div> </div>
@@ -906,6 +912,7 @@
<input type="date" id="logEndDate" style="width:150px"> <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-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 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>
<div class="grid grid-cols-5 gap-3 mb-4"> <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()"> <div class="stat-card" style="padding:14px;text-align:center;cursor:pointer" onclick="document.getElementById('logTypeFilter').value='location';loadDataLog()">
@@ -935,6 +942,7 @@
<table> <table>
<thead> <thead>
<tr> <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>ID</th>
<th>类型</th> <th>类型</th>
<th>设备ID</th> <th>设备ID</th>
@@ -946,7 +954,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="datalogTableBody"> <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> </tbody>
</table> </table>
</div> </div>
@@ -1902,32 +1910,44 @@
} }
} }
function toggleAllLocCheckboxes(checked) { // ==================== GENERIC BATCH DELETE HELPERS ====================
document.querySelectorAll('.loc-sel-cb').forEach(cb => { cb.checked = checked; }); function toggleAllCheckboxes(cbClass, countSpanId, btnId, checked) {
updateLocSelCount(); document.querySelectorAll('.' + cbClass).forEach(cb => { cb.checked = checked; });
updateSelCount(cbClass, countSpanId, btnId);
} }
function updateSelCount(cbClass, countSpanId, btnId) {
function updateLocSelCount() { const count = document.querySelectorAll('.' + cbClass + ':checked').length;
const count = document.querySelectorAll('.loc-sel-cb:checked').length; document.getElementById(countSpanId).textContent = count;
document.getElementById('locSelCount').textContent = count; document.getElementById(btnId).disabled = count === 0;
document.getElementById('btnBatchDeleteLoc').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() { async function _batchDelete(cbClass, apiPath, idKey, label, reloadFn) {
const ids = Array.from(document.querySelectorAll('.loc-sel-cb:checked')).map(cb => parseInt(cb.value)); const ids = Array.from(document.querySelectorAll('.' + cbClass + ':checked')).map(cb => parseInt(cb.value));
if (ids.length === 0) { showToast('请先勾选要删除的记录', 'info'); return; } if (ids.length === 0) { showToast('请先勾选要删除的记录', 'info'); return; }
if (!confirm(`确定批量删除选中的 ${ids.length}位置记录`)) return; if (!confirm(`确定批量删除选中的 ${ids.length}${label}`)) return;
try { try {
const result = await apiCall(`${API_BASE}/locations/batch-delete`, { const result = await apiCall(`${API_BASE}/${apiPath}`, {
method: 'POST', method: 'POST', body: JSON.stringify({ [idKey]: ids }),
body: JSON.stringify({ location_ids: ids }),
}); });
showToast(`已删除 ${result.deleted} 条记录`); showToast(`已删除 ${result.deleted} 条记录`);
loadLocationRecords(); reloadFn();
} catch (err) { } catch (err) {
showToast('批量删除失败: ' + err.message, 'error'); 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() { async function batchDeleteNoCoordLocations() {
const deviceId = document.getElementById('locDeviceSelect').value || null; const deviceId = document.getElementById('locDeviceSelect').value || null;
@@ -2608,10 +2628,11 @@
const tbody = document.getElementById('alarmsTableBody'); const tbody = document.getElementById('alarmsTableBody');
if (items.length === 0) { 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 { } else {
tbody.innerHTML = items.map(a => ` tbody.innerHTML = items.map(a => `
<tr> <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 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><span class="${alarmTypeClass(a.alarm_type)} font-semibold">${alarmTypeName(a.alarm_type)}</span></td>
<td>${escapeHtml(a.alarm_source || '-')}</td> <td>${escapeHtml(a.alarm_source || '-')}</td>
@@ -2629,10 +2650,11 @@
</tr> </tr>
`).join(''); `).join('');
} }
document.getElementById('alarmSelectAll').checked = false;
renderPagination('alarmsPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadAlarms'); renderPagination('alarmsPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadAlarms');
} catch (err) { } catch (err) {
showToast('加载告警失败: ' + err.message, 'error'); 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 { } finally {
hideLoading('alarmsLoading'); hideLoading('alarmsLoading');
} }
@@ -2698,7 +2720,7 @@
const tbody = document.getElementById('attendanceTableBody'); const tbody = document.getElementById('attendanceTableBody');
if (items.length === 0) { 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 { } else {
tbody.innerHTML = items.map(a => { tbody.innerHTML = items.map(a => {
const posStr = a.address || (a.latitude != null ? `${Number(a.latitude).toFixed(6)}, ${Number(a.longitude).toFixed(6)}` : '-'); 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 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'; const srcColor = {'device':'#9ca3af','bluetooth':'#818cf8','fence':'#34d399'}[a.attendance_source] || '#9ca3af';
return `<tr> 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 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><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> <td style="color:${srcColor};font-size:12px">${srcLabel}</td>
@@ -2720,10 +2743,11 @@
</tr>`; </tr>`;
}).join(''); }).join('');
} }
document.getElementById('attSelectAll').checked = false;
renderPagination('attendancePagination', data.total || 0, data.page || p, data.page_size || ps, 'loadAttendance'); renderPagination('attendancePagination', data.total || 0, data.page || p, data.page_size || ps, 'loadAttendance');
} catch (err) { } catch (err) {
showToast('加载考勤记录失败: ' + err.message, 'error'); 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 { } finally {
hideLoading('attendanceLoading'); hideLoading('attendanceLoading');
} }
@@ -2764,7 +2788,7 @@
const tbody = document.getElementById('bluetoothTableBody'); const tbody = document.getElementById('bluetoothTableBody');
if (items.length === 0) { 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 { } else {
tbody.innerHTML = items.map(b => { 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>'; 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 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>` : '-'; 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> 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 class="font-mono text-xs">${escapeHtml(b.device_id || '-')}</td>
<td>${typeIcon} <span class="font-semibold">${typeName}</span></td> <td>${typeIcon} <span class="font-semibold">${typeName}</span></td>
<td class="font-mono text-xs">${escapeHtml(mac)}</td> <td class="font-mono text-xs">${escapeHtml(mac)}</td>
@@ -2788,10 +2813,11 @@
</tr>`; </tr>`;
}).join(''); }).join('');
} }
document.getElementById('btSelectAll').checked = false;
renderPagination('bluetoothPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadBluetooth'); renderPagination('bluetoothPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadBluetooth');
} catch (err) { } catch (err) {
showToast('加载蓝牙记录失败: ' + err.message, 'error'); 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 { } finally {
hideLoading('bluetoothLoading'); hideLoading('bluetoothLoading');
} }
@@ -2919,7 +2945,7 @@
const tbody = document.getElementById('datalogTableBody'); const tbody = document.getElementById('datalogTableBody');
if (items.length === 0) { 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 { } else {
tbody.innerHTML = items.map(r => { tbody.innerHTML = items.map(r => {
const detail = _logDetail(type, 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 time = r.recorded_at || r.alarm_time || r.heartbeat_time || r.created_at;
const typeBadge = _logTypeBadge(type); const typeBadge = _logTypeBadge(type);
return `<tr> 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 class="font-mono text-xs">${r.id}</td>
<td>${typeBadge}</td> <td>${typeBadge}</td>
<td>${r.device_id || '-'}</td> <td>${r.device_id || '-'}</td>
@@ -2941,9 +2968,10 @@
</tr>`; </tr>`;
}).join(''); }).join('');
} }
document.getElementById('logSelectAll').checked = false;
renderPagination('datalogPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadDataLog'); renderPagination('datalogPagination', data.total || 0, data.page || p, data.page_size || ps, 'loadDataLog');
} catch (err) { } 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 { } finally {
hideLoading('datalogLoading'); hideLoading('datalogLoading');
} }