feat: 管理后台UI全面优化 + TCP指令发送修复
- 指令页面:三个设备选择器合并为顶部统一选择器,在线/离线分组显示 - 仪表盘:8卡片压缩为6紧凑卡片,新增刷新按钮和更新时间戳 - 仪表盘:最近告警添加"查看全部"跳转链接 - 考勤页面:定位方式显示GPS/WiFi/LBS文字标签替代纯图标 - 告警页面:alarm_source中文化(单围栏/多围栏/基站/WiFi) - 告警页面:确认操作改为DOM单行更新,无需刷新整个表格 - 数据日志:默认选中"位置"类型 - 全部表格:全选复选框tooltip改为"全选当前页" - TCP修复:移除发送前硬性在线检查,改为尝试发送+失败提示 - TCP修复:检测僵死连接(writer.is_closing)并自动清理 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -103,12 +103,6 @@ async def _send_to_device(
|
||||
executor : async callable
|
||||
The actual send function, e.g. tcp_command_service.send_command(...)
|
||||
"""
|
||||
if not tcp_command_service.is_device_online(device.imei):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Device {device.imei} is not online / 设备 {device.imei} 不在线",
|
||||
)
|
||||
|
||||
command_log = await command_service.create_command(
|
||||
db,
|
||||
device_id=device.id,
|
||||
@@ -117,7 +111,7 @@ async def _send_to_device(
|
||||
)
|
||||
|
||||
try:
|
||||
await executor()
|
||||
result = await executor()
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).error("Command send failed: %s", e)
|
||||
command_log.status = "failed"
|
||||
@@ -128,6 +122,15 @@ async def _send_to_device(
|
||||
detail=fail_msg,
|
||||
)
|
||||
|
||||
if result is False:
|
||||
command_log.status = "failed"
|
||||
await db.flush()
|
||||
await db.refresh(command_log)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Device {device.imei} TCP not connected / 设备 {device.imei} TCP未连接,请等待设备重连",
|
||||
)
|
||||
|
||||
command_log.status = "sent"
|
||||
command_log.sent_at = now_cst()
|
||||
await db.flush()
|
||||
|
||||
@@ -226,97 +226,37 @@
|
||||
<div class="guide-tips"><p><i class="fas fa-sync-alt"></i> 数据每 30 秒自动刷新,无需手动操作</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">设备总数</p>
|
||||
<p class="text-3xl font-bold mt-1" id="dashTotalDevices">-</p>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-gray-500" id="dashLastUpdated"></span>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-microchip text-blue-400 text-xl"></i>
|
||||
<button class="btn btn-sm" style="padding:4px 12px;font-size:12px;" onclick="loadDashboard()"><i class="fas fa-sync-alt mr-1"></i>刷新</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
||||
<div class="stat-card" style="padding:16px;">
|
||||
<p class="text-gray-400 text-xs mb-1"><i class="fas fa-microchip text-blue-400 mr-1"></i>设备总数</p>
|
||||
<p class="text-2xl font-bold" id="dashTotalDevices">-</p>
|
||||
</div>
|
||||
<div class="stat-card" style="padding:16px;">
|
||||
<p class="text-gray-400 text-xs mb-1"><i class="fas fa-wifi text-green-400 mr-1"></i>在线</p>
|
||||
<p class="text-2xl font-bold text-green-400" id="dashOnlineDevices">-</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">在线设备</p>
|
||||
<p class="text-3xl font-bold mt-1 text-green-400" id="dashOnlineDevices">-</p>
|
||||
<div class="stat-card" style="padding:16px;">
|
||||
<p class="text-gray-400 text-xs mb-1"><i class="fas fa-plug text-red-400 mr-1"></i>离线</p>
|
||||
<p class="text-2xl font-bold text-red-400" id="dashOfflineDevices">-</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-wifi text-green-400 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">离线设备</p>
|
||||
<p class="text-3xl font-bold mt-1 text-red-400" id="dashOfflineDevices">-</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-red-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-plug text-red-400 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">已连接设备</p>
|
||||
<p class="text-3xl font-bold mt-1 text-purple-400" id="dashConnectedDevices">-</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-link text-purple-400 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">告警总数</p>
|
||||
<p class="text-3xl font-bold mt-1 text-yellow-400" id="dashTotalAlarms">-</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-yellow-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-bell text-yellow-400 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">未确认告警</p>
|
||||
<p class="text-3xl font-bold mt-1 text-orange-400" id="dashUnackAlarms">-</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-orange-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-orange-400 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">已确认告警</p>
|
||||
<p class="text-3xl font-bold mt-1 text-teal-400" id="dashAckAlarms">-</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-teal-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-check-circle text-teal-400 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">系统状态</p>
|
||||
<p class="text-lg font-bold mt-1 text-green-400" id="dashSystemStatus">-</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-900/50 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-heartbeat text-green-400 text-xl"></i>
|
||||
<div class="stat-card" style="padding:16px;">
|
||||
<p class="text-gray-400 text-xs mb-1"><i class="fas fa-bell text-yellow-400 mr-1"></i>告警总数</p>
|
||||
<p class="text-2xl font-bold text-yellow-400" id="dashTotalAlarms">-</p>
|
||||
</div>
|
||||
<div class="stat-card" style="padding:16px;">
|
||||
<p class="text-gray-400 text-xs mb-1"><i class="fas fa-exclamation-triangle text-orange-400 mr-1"></i>未确认</p>
|
||||
<p class="text-2xl font-bold text-orange-400" id="dashUnackAlarms">-</p>
|
||||
</div>
|
||||
<div class="stat-card" style="padding:16px;">
|
||||
<p class="text-gray-400 text-xs mb-1"><i class="fas fa-heartbeat text-green-400 mr-1"></i>系统</p>
|
||||
<p class="text-lg font-bold text-green-400 mt-1" id="dashSystemStatus">-</p>
|
||||
<span class="text-xs text-gray-500" id="dashConnectedHint"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -328,7 +268,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3 class="text-lg font-semibold mb-4"><i class="fas fa-list mr-2 text-red-400"></i>最近告警</h3>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold"><i class="fas fa-list mr-2 text-red-400"></i>最近告警</h3>
|
||||
<a href="#" onclick="event.preventDefault();switchPage('alarms')" class="text-xs text-blue-400 hover:text-blue-300">查看全部 <i class="fas fa-arrow-right ml-1"></i></a>
|
||||
</div>
|
||||
<div id="dashRecentAlarms" class="space-y-2" style="max-height: 300px; overflow-y: auto;">
|
||||
<p class="text-gray-500 text-sm">加载中...</p>
|
||||
</div>
|
||||
@@ -468,7 +411,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px"><input type="checkbox" id="locSelectAll" onchange="toggleAllLocCheckboxes(this.checked)" title="全选"></th>
|
||||
<th style="width:32px"><input type="checkbox" id="locSelectAll" onchange="toggleAllLocCheckboxes(this.checked)" title="全选当前页"></th>
|
||||
<th>设备ID</th>
|
||||
<th>类型</th>
|
||||
<th>纬度</th>
|
||||
@@ -561,7 +504,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 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>
|
||||
@@ -645,7 +588,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 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>
|
||||
@@ -713,7 +656,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 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>
|
||||
@@ -902,7 +845,7 @@
|
||||
<select id="logDeviceFilter" style="width:160px"><option value="">全部设备</option></select>
|
||||
<select id="logTypeFilter" style="width:140px">
|
||||
<option value="">全部类型</option>
|
||||
<option value="location">位置</option>
|
||||
<option value="location" selected>位置</option>
|
||||
<option value="alarm">告警</option>
|
||||
<option value="heartbeat">心跳</option>
|
||||
<option value="attendance">考勤</option>
|
||||
@@ -942,7 +885,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 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>
|
||||
@@ -980,15 +923,16 @@
|
||||
<div class="guide-tips"><p><i class="fas fa-exclamation-triangle"></i> 所有下发功能仅对当前在线(TCP 已连接)设备有效,离线设备无法接收</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card mb-4" style="display:flex;align-items:center;gap:16px;padding:12px 20px;">
|
||||
<label style="white-space:nowrap;font-weight:600;"><i class="fas fa-broadcast-tower mr-2 text-cyan-400"></i>目标设备</label>
|
||||
<select id="cmdUnifiedDevice" style="flex:1;max-width:360px;">
|
||||
<option value="">选择设备...</option>
|
||||
</select>
|
||||
<span id="cmdDeviceStatus" class="text-xs text-gray-500"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<div class="stat-card">
|
||||
<h3 class="text-lg font-semibold mb-4"><i class="fas fa-paper-plane mr-2 text-blue-400"></i>发送指令</h3>
|
||||
<div class="form-group">
|
||||
<label>目标设备</label>
|
||||
<select id="cmdDeviceSelect">
|
||||
<option value="">选择设备...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>指令类型</label>
|
||||
<input type="text" id="cmdType" placeholder="如: LOCATE, POWEROFF, RESTART...">
|
||||
@@ -1001,12 +945,6 @@
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3 class="text-lg font-semibold mb-4"><i class="fas fa-comment mr-2 text-green-400"></i>发送消息</h3>
|
||||
<div class="form-group">
|
||||
<label>目标设备</label>
|
||||
<select id="msgDeviceSelect">
|
||||
<option value="">选择设备...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>消息内容</label>
|
||||
<textarea id="msgContent" rows="5" placeholder="输入要发送给设备的消息..."></textarea>
|
||||
@@ -1015,12 +953,6 @@
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3 class="text-lg font-semibold mb-4"><i class="fas fa-volume-up mr-2 text-purple-400"></i>语音播报</h3>
|
||||
<div class="form-group">
|
||||
<label>目标设备</label>
|
||||
<select id="ttsDeviceSelect">
|
||||
<option value="">选择设备...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>播报文本</label>
|
||||
<textarea id="ttsContent" rows="3" placeholder="输入语音播报内容,设备将通过 TTS 引擎朗读..."></textarea>
|
||||
@@ -1204,7 +1136,7 @@
|
||||
low_battery: '低电量', low_battery_protection: '低电保护',
|
||||
sim_change: 'SIM卡更换', power_off: '关机',
|
||||
airplane_mode: '飞行模式', remove: '拆除',
|
||||
door: '门', shutdown: '关机',
|
||||
door: '门', shutdown: '低电关机',
|
||||
voice_alarm: '声控报警', fake_base_station: '伪基站',
|
||||
cover_open: '开盖', internal_low_battery: '内部低电',
|
||||
acc_on: 'ACC开', acc_off: 'ACC关',
|
||||
@@ -1506,7 +1438,7 @@
|
||||
const data = await apiCall(`${API_BASE}/devices?page=1&page_size=100`);
|
||||
const devices = data.items || [];
|
||||
cachedDevices = devices;
|
||||
const selectors = ['locDeviceSelect', 'alarmDeviceFilter', 'attDeviceFilter', 'btDeviceFilter', 'cmdDeviceSelect', 'msgDeviceSelect', 'ttsDeviceSelect', 'cmdHistoryDeviceFilter'];
|
||||
const selectors = ['locDeviceSelect', 'alarmDeviceFilter', 'attDeviceFilter', 'btDeviceFilter', 'cmdHistoryDeviceFilter'];
|
||||
selectors.forEach(id => {
|
||||
const sel = document.getElementById(id);
|
||||
if (!sel) return;
|
||||
@@ -1521,6 +1453,51 @@
|
||||
});
|
||||
if (currentVal) sel.value = currentVal;
|
||||
});
|
||||
// Unified command device selector with online/offline status
|
||||
const cmdSel = document.getElementById('cmdUnifiedDevice');
|
||||
if (cmdSel) {
|
||||
const curVal = cmdSel.value;
|
||||
cmdSel.innerHTML = '<option value="">选择设备...</option>';
|
||||
const online = devices.filter(d => d.status === 'online');
|
||||
const offline = devices.filter(d => d.status !== 'online');
|
||||
if (online.length) {
|
||||
const grpOn = document.createElement('optgroup');
|
||||
grpOn.label = `在线 (${online.length})`;
|
||||
online.forEach(d => {
|
||||
const o = document.createElement('option');
|
||||
o.value = d.id || d.device_id || '';
|
||||
o.textContent = `🟢 ${d.name || d.imei} (${d.imei || ''})`;
|
||||
grpOn.appendChild(o);
|
||||
});
|
||||
cmdSel.appendChild(grpOn);
|
||||
}
|
||||
if (offline.length) {
|
||||
const grpOff = document.createElement('optgroup');
|
||||
grpOff.label = `离线 (${offline.length})`;
|
||||
offline.forEach(d => {
|
||||
const o = document.createElement('option');
|
||||
o.value = d.id || d.device_id || '';
|
||||
o.textContent = `⚪ ${d.name || d.imei} (${d.imei || ''})`;
|
||||
o.style.color = '#6b7280';
|
||||
grpOff.appendChild(o);
|
||||
});
|
||||
cmdSel.appendChild(grpOff);
|
||||
}
|
||||
if (curVal) cmdSel.value = curVal;
|
||||
// Update status hint on change
|
||||
cmdSel.onchange = () => {
|
||||
const st = document.getElementById('cmdDeviceStatus');
|
||||
if (!st) return;
|
||||
const did = cmdSel.value;
|
||||
if (!did) { st.textContent = ''; return; }
|
||||
const dev = devices.find(d => String(d.id) === did || String(d.device_id) === did);
|
||||
if (dev && dev.status === 'online') {
|
||||
st.innerHTML = '<span style="color:#22c55e"><i class="fas fa-circle" style="font-size:8px;vertical-align:middle;margin-right:4px"></i>在线 - 可下发</span>';
|
||||
} else {
|
||||
st.innerHTML = '<span style="color:#ef4444"><i class="fas fa-circle" style="font-size:8px;vertical-align:middle;margin-right:4px"></i>离线 - 无法接收指令</span>';
|
||||
}
|
||||
};
|
||||
}
|
||||
// Render device panel on locations page
|
||||
if (currentPage === 'locations' && document.getElementById('locPanelList')) {
|
||||
const sorted = sortDevicesByActivity(devices);
|
||||
@@ -1556,7 +1533,6 @@
|
||||
const as = alarmStats.value;
|
||||
animateCounter('dashTotalAlarms', as.total || 0);
|
||||
animateCounter('dashUnackAlarms', as.unacknowledged || 0);
|
||||
animateCounter('dashAckAlarms', as.acknowledged || 0);
|
||||
renderAlarmDoughnut('dashAlarmChart', as.by_type, false);
|
||||
}
|
||||
|
||||
@@ -1564,14 +1540,19 @@
|
||||
const h = health.value;
|
||||
const status = h.status || 'unknown';
|
||||
const connected = h.connected_devices || 0;
|
||||
animateCounter('dashConnectedDevices', connected);
|
||||
document.getElementById('dashSystemStatus').textContent = status === 'ok' || status === 'healthy' ? '正常运行' : status;
|
||||
const hint = document.getElementById('dashConnectedHint');
|
||||
if (hint) hint.textContent = `TCP ${connected} 台`;
|
||||
document.getElementById('healthStatus').innerHTML = `<i class="fas fa-circle text-green-500 mr-1 text-xs"></i>在线 (${connected}台)`;
|
||||
} else {
|
||||
document.getElementById('healthStatus').innerHTML = `<i class="fas fa-circle text-red-500 mr-1 text-xs"></i>离线`;
|
||||
document.getElementById('dashSystemStatus').textContent = '无法连接';
|
||||
}
|
||||
|
||||
// Update last-refreshed timestamp
|
||||
const upd = document.getElementById('dashLastUpdated');
|
||||
if (upd) upd.textContent = '更新于 ' + new Date().toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||||
|
||||
// Load recent alarms
|
||||
try {
|
||||
const alarms = await apiCall(`${API_BASE}/alarms?page=1&page_size=10`);
|
||||
@@ -1611,8 +1592,28 @@
|
||||
const labels = [];
|
||||
const values = [];
|
||||
const colors = [];
|
||||
const colorMap = { sos: '#ef4444', low_battery: '#f97316', remove: '#eab308', fence: '#a855f7', fall: '#3b82f6' };
|
||||
const nameMap = { sos: 'SOS', low_battery: '低电量', remove: '拆除', fence: '围栏', fall: '跌倒' };
|
||||
const colorMap = {
|
||||
sos: '#ef4444', power_cut: '#dc2626', vibration: '#f97316', low_battery: '#fb923c',
|
||||
low_battery_protection: '#fdba74', internal_low_battery: '#fbbf24',
|
||||
enter_fence: '#a855f7', exit_fence: '#c084fc', displacement: '#8b5cf6',
|
||||
over_speed: '#f43f5e', remove: '#eab308', cover_open: '#facc15',
|
||||
power_on: '#22c55e', power_off: '#64748b', shutdown: '#475569',
|
||||
sim_change: '#06b6d4', airplane_mode: '#0ea5e9',
|
||||
enter_gps_dead_zone: '#94a3b8', exit_gps_dead_zone: '#cbd5e1',
|
||||
gps_first_fix: '#34d399', door: '#14b8a6',
|
||||
voice_alarm: '#e879f9', fake_base_station: '#f472b6',
|
||||
normal: '#6b7280', acc_on: '#10b981', acc_off: '#9ca3af',
|
||||
};
|
||||
const nameMap = {
|
||||
normal: '正常', sos: 'SOS', power_cut: '断电', vibration: '振动',
|
||||
enter_fence: '进围栏', exit_fence: '出围栏', over_speed: '超速',
|
||||
displacement: '位移', enter_gps_dead_zone: '进GPS盲区', exit_gps_dead_zone: '出GPS盲区',
|
||||
power_on: '开机', gps_first_fix: 'GPS首次定位', low_battery: '低电量',
|
||||
low_battery_protection: '低电保护', sim_change: 'SIM更换', power_off: '关机',
|
||||
airplane_mode: '飞行模式', remove: '拆除', door: '门', shutdown: '低电关机',
|
||||
voice_alarm: '声控', fake_base_station: '伪基站', cover_open: '开盖',
|
||||
internal_low_battery: '内部低电', acc_on: 'ACC开', acc_off: 'ACC关',
|
||||
};
|
||||
|
||||
if (byType && typeof byType === 'object') {
|
||||
Object.entries(byType).forEach(([key, val]) => {
|
||||
@@ -2635,7 +2636,7 @@
|
||||
<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>
|
||||
<td>${({'single_fence':'<span style="color:#a855f7"><i class="fas fa-draw-polygon mr-1"></i>单围栏</span>','multi_fence':'<span style="color:#c084fc"><i class="fas fa-layer-group mr-1"></i>多围栏</span>','lbs':'<span style="color:#f97316"><i class="fas fa-broadcast-tower mr-1"></i>基站</span>','wifi':'<span style="color:#f59e0b"><i class="fas fa-wifi mr-1"></i>WiFi</span>'})[a.alarm_source] || escapeHtml(a.alarm_source || '-')}</td>
|
||||
<td class="text-xs">${a.address ? escapeHtml(a.address) : (a.latitude != null ? Number(a.latitude).toFixed(6) + ', ' + Number(a.longitude).toFixed(6) : '-')}</td>
|
||||
<td>${a.battery_level != null ? a.battery_level + '%' : '-'}</td>
|
||||
<td>${a.gsm_signal != null ? a.gsm_signal : '-'}</td>
|
||||
@@ -2667,7 +2668,17 @@
|
||||
body: JSON.stringify({ acknowledged }),
|
||||
});
|
||||
showToast(acknowledged ? '告警已确认' : '告警已取消确认');
|
||||
loadAlarms();
|
||||
// Update row in-place instead of full reload
|
||||
const cb = document.querySelector(`.alarm-sel-cb[value="${id}"]`);
|
||||
if (cb) {
|
||||
const tr = cb.closest('tr');
|
||||
const cells = tr.querySelectorAll('td');
|
||||
// col 7 = acknowledged badge, col 9 = action button
|
||||
if (cells[7]) cells[7].innerHTML = acknowledged ? '<span class="badge badge-online">已确认</span>' : '<span class="badge badge-offline">未确认</span>';
|
||||
if (cells[9]) cells[9].innerHTML = acknowledged
|
||||
? `<button class="btn btn-warning btn-sm" onclick="toggleAlarmAck('${id}', false)"><i class="fas fa-undo"></i></button>`
|
||||
: `<button class="btn btn-success btn-sm" onclick="toggleAlarmAck('${id}', true)"><i class="fas fa-check"></i></button>`;
|
||||
}
|
||||
loadAlarmStats();
|
||||
} catch (err) {
|
||||
showToast('操作失败: ' + err.message, 'error');
|
||||
@@ -2724,7 +2735,7 @@
|
||||
} else {
|
||||
tbody.innerHTML = items.map(a => {
|
||||
const posStr = a.address || (a.latitude != null ? `${Number(a.latitude).toFixed(6)}, ${Number(a.longitude).toFixed(6)}` : '-');
|
||||
const gpsIcon = a.gps_positioned ? '<i class="fas fa-satellite text-green-400" title="GPS定位"></i>' : '<i class="fas fa-broadcast-tower text-yellow-400" title="基站/WiFi定位"></i>';
|
||||
const locMethod = a.gps_positioned ? '<span style="color:#22c55e;font-size:11px"><i class="fas fa-satellite mr-1"></i>GPS</span>' : (a.wifi_data ? '<span style="color:#f59e0b;font-size:11px"><i class="fas fa-wifi mr-1"></i>WiFi</span>' : '<span style="color:#f97316;font-size:11px"><i class="fas fa-broadcast-tower mr-1"></i>LBS</span>');
|
||||
const battStr = a.battery_level != null ? `${a.battery_level}%` : '-';
|
||||
const sigStr = a.gsm_signal != null ? `GSM:${a.gsm_signal}` : '';
|
||||
const lbsStr = a.mcc != null ? `${a.mcc}/${a.mnc || 0}/${a.lac || 0}/${a.cell_id || 0}` : '-';
|
||||
@@ -2735,7 +2746,7 @@
|
||||
<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>
|
||||
<td class="text-xs">${gpsIcon} ${escapeHtml(posStr)}</td>
|
||||
<td class="text-xs">${locMethod} ${escapeHtml(posStr)}</td>
|
||||
<td class="text-xs">${battStr} ${sigStr}</td>
|
||||
<td class="text-xs font-mono">${lbsStr}</td>
|
||||
<td class="text-xs text-gray-400">${formatTime(a.recorded_at)}</td>
|
||||
@@ -3979,7 +3990,7 @@
|
||||
|
||||
// ==================== COMMANDS ====================
|
||||
async function sendCommand() {
|
||||
const deviceId = document.getElementById('cmdDeviceSelect').value;
|
||||
const deviceId = document.getElementById('cmdUnifiedDevice').value;
|
||||
const commandType = document.getElementById('cmdType').value.trim();
|
||||
const commandContent = document.getElementById('cmdContent').value.trim();
|
||||
|
||||
@@ -4001,7 +4012,7 @@
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const deviceId = document.getElementById('msgDeviceSelect').value;
|
||||
const deviceId = document.getElementById('cmdUnifiedDevice').value;
|
||||
const message = document.getElementById('msgContent').value.trim();
|
||||
|
||||
if (!deviceId) { showToast('请选择设备', 'error'); return; }
|
||||
@@ -4034,7 +4045,7 @@
|
||||
}
|
||||
|
||||
async function sendTTS() {
|
||||
const deviceId = document.getElementById('ttsDeviceSelect').value;
|
||||
const deviceId = document.getElementById('cmdUnifiedDevice').value;
|
||||
const text = document.getElementById('ttsContent').value.trim();
|
||||
|
||||
if (!deviceId) { showToast('请选择目标设备', 'error'); return; }
|
||||
|
||||
@@ -2445,6 +2445,13 @@ class TCPManager:
|
||||
return False
|
||||
|
||||
_reader, writer, conn_info = conn
|
||||
|
||||
# Check if the writer is still alive
|
||||
if writer.is_closing():
|
||||
logger.warning("IMEI=%s writer is closing, removing stale connection", imei)
|
||||
del self.connections[imei]
|
||||
return False
|
||||
|
||||
serial = conn_info.next_serial()
|
||||
|
||||
# Build 0x80 online-command packet
|
||||
@@ -2469,7 +2476,8 @@ class TCPManager:
|
||||
command_content,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to send command to IMEI=%s", imei)
|
||||
logger.exception("Failed to send command to IMEI=%s, removing stale connection", imei)
|
||||
self.connections.pop(imei, None)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -2495,6 +2503,12 @@ class TCPManager:
|
||||
return False
|
||||
|
||||
_reader, writer, conn_info = conn
|
||||
|
||||
if writer.is_closing():
|
||||
logger.warning("IMEI=%s writer is closing, removing stale connection", imei)
|
||||
del self.connections[imei]
|
||||
return False
|
||||
|
||||
serial = conn_info.next_serial()
|
||||
|
||||
msg_bytes = message.encode("utf-16-be")
|
||||
@@ -2517,7 +2531,8 @@ class TCPManager:
|
||||
logger.info("Message sent to IMEI=%s (%d bytes)", imei, len(msg_bytes))
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Failed to send message to IMEI=%s", imei)
|
||||
logger.exception("Failed to send message to IMEI=%s, removing stale connection", imei)
|
||||
self.connections.pop(imei, None)
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user