Files
iot-device-management-service/app/static/alarm_detail.html
16337 1d84456c0f feat: 重写H5告警详情页,实现状态感知的交互设计
- 待处理: 显示3个按钮(前往处理/已处理/误报忽略)
- 处理中: 显示2个按钮(已处理/误报忽略)
- 已处理/已忽略/自动关闭: 隐藏按钮,显示结果条
- 添加确认弹窗防止误操作
- 优化移动端显示(word-break, 圆角等)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:18:01 +08:00

255 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>告警详情</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 420px; margin: 0 auto; padding: 12px; }
/* 截图区 */
.snapshot { width: 100%; border-radius: 8px; overflow: hidden; background: #000; margin-bottom: 12px; }
.snapshot img { width: 100%; display: block; }
.snapshot .no-img { padding: 60px 0; text-align: center; color: #999; font-size: 14px; }
/* 告警信息 */
.info-card { background: #fff; border-radius: 8px; padding: 16px; margin-bottom: 12px; }
.level-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; color: #fff; margin-bottom: 8px; }
.level-1 { background: #1890ff; }
.level-2 { background: #faad14; }
.level-3 { background: #fa541c; }
.level-4 { background: #f5222d; }
.info-card .title { font-size: 16px; font-weight: 600; margin-bottom: 12px; }
.info-row { display: flex; padding: 6px 0; font-size: 14px; border-bottom: 1px solid #f0f0f0; }
.info-row:last-child { border-bottom: none; }
.info-row .label { color: #999; width: 70px; flex-shrink: 0; }
.info-row .value { color: #333; flex: 1; word-break: break-all; }
/* VLM 描述 */
.vlm-desc { background: #f0f7ff; border-left: 3px solid #1890ff; padding: 10px 12px; border-radius: 0 6px 6px 0; margin-bottom: 12px; font-size: 13px; color: #555; }
.vlm-desc .tag { font-size: 11px; color: #1890ff; margin-bottom: 4px; }
/* 状态条 */
.status-bar { background: #fff; border-radius: 8px; padding: 14px 16px; margin-bottom: 12px; }
.status-inner { display: flex; align-items: center; justify-content: center; gap: 8px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-pending { background: #fa541c; }
.dot-handling { background: #faad14; }
.dot-done { background: #52c41a; }
.dot-false { background: #999; }
.status-text { font-size: 14px; font-weight: 500; }
.status-sub { font-size: 12px; color: #999; text-align: center; margin-top: 4px; }
/* 结果条(终态显示) */
.result-bar { background: #fff; border-radius: 8px; padding: 16px; margin-bottom: 12px; text-align: center; }
.result-bar .result-icon { font-size: 32px; margin-bottom: 6px; }
.result-bar .result-text { font-size: 15px; font-weight: 500; margin-bottom: 4px; }
.result-bar .result-sub { font-size: 12px; color: #999; }
.result-done { border-left: 4px solid #52c41a; }
.result-false { border-left: 4px solid #999; }
.result-auto { border-left: 4px solid #1890ff; }
/* 按钮组 */
.actions { display: flex; gap: 10px; padding: 0 0 20px; }
.btn { flex: 1; padding: 12px 0; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: opacity 0.2s; }
.btn:active { opacity: 0.7; }
.btn-go { background: #1890ff; color: #fff; }
.btn-done { background: #52c41a; color: #fff; }
.btn-false { background: #f5f5f5; color: #666; border: 1px solid #d9d9d9; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* 弹窗 */
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 50; display: none; align-items: center; justify-content: center; }
.modal-mask.show { display: flex; }
.modal-box { background: #fff; border-radius: 12px; width: 280px; padding: 24px 20px 16px; text-align: center; }
.modal-box .modal-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
.modal-box .modal-desc { font-size: 13px; color: #666; margin-bottom: 16px; }
.modal-btns { display: flex; gap: 10px; }
.modal-btns .btn { font-size: 14px; padding: 10px 0; }
.btn-cancel { background: #f5f5f5; color: #666; }
.btn-confirm-modal { background: #1890ff; color: #fff; }
.toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.75); color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px; display: none; z-index: 100; }
.loading { text-align: center; padding: 80px 0; color: #999; font-size: 14px; }
</style>
</head>
<body>
<div class="container" id="app">
<div class="loading">加载中...</div>
</div>
<div class="modal-mask" id="modal">
<div class="modal-box">
<div class="modal-title" id="modal-title"></div>
<div class="modal-desc" id="modal-desc"></div>
<div class="modal-btns">
<button class="btn btn-cancel" onclick="closeModal()">取消</button>
<button class="btn btn-confirm-modal" id="modal-confirm">确定</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
var params = new URLSearchParams(location.search);
var alarmId = params.get('alarm_id');
var baseUrl = location.origin;
var alarmData = null;
var pendingAction = null;
var typeNames = { leave_post: '人员离岗', intrusion: '周界入侵' };
var levelNames = { 1: '提醒', 2: '一般', 3: '严重', 4: '紧急' };
function getDisplayStatus(d) {
var as = d.alarm_status || 'NEW';
var hs = d.handle_status || 'UNHANDLED';
if (as === 'FALSE') return { label: '已忽略', sub: '误报', dot: 'dot-false', type: 'false' };
if (as === 'CLOSED' && hs === 'DONE') {
var remark = d.handle_remark || '';
if (remark.indexOf('自动') >= 0) return { label: '已自动关闭', sub: remark, dot: 'dot-done', type: 'auto' };
return { label: '已处理', sub: remark || '已完成', dot: 'dot-done', type: 'done' };
}
if (as === 'CONFIRMED' || hs === 'HANDLING') return { label: '处理中', sub: (d.handler || '') + ' 正在处理', dot: 'dot-handling', type: 'handling' };
return { label: '待处理', sub: '等待安保人员处理', dot: 'dot-pending', type: 'pending' };
}
function loadAlarm() {
if (!alarmId) {
document.getElementById('app').innerHTML = '<div class="loading">缺少告警ID参数</div>';
return;
}
fetch(baseUrl + '/api/wechat/alarm_detail?alarm_id=' + encodeURIComponent(alarmId))
.then(function(r) { return r.json(); })
.then(function(json) {
if (json.code !== 0) throw new Error(json.msg || '加载失败');
alarmData = json.data;
render();
})
.catch(function(e) {
document.getElementById('app').innerHTML = '<div class="loading" style="color:#f00;">加载失败: ' + e.message + '</div>';
});
}
function render() {
var d = alarmData;
var typeName = typeNames[d.alarm_type] || d.alarm_type;
var levelName = levelNames[d.alarm_level] || '一般';
var levelClass = 'level-' + (d.alarm_level || 2);
var status = getDisplayStatus(d);
var html = '';
// 截图
html += '<div class="snapshot">';
if (d.snapshot_url) {
html += '<img src="' + d.snapshot_url + '" alt="告警截图" onerror="this.parentElement.innerHTML=\'<div class=no-img>截图加载失败</div>\'">';
} else {
html += '<div class="no-img">暂无截图</div>';
}
html += '</div>';
// 信息卡片
html += '<div class="info-card">';
html += '<span class="level-badge ' + levelClass + '">' + levelName + '</span>';
html += '<div class="title">' + typeName + '告警</div>';
html += '<div class="info-row"><span class="label">告警ID</span><span class="value">' + (d.alarm_id || '').slice(-12) + '</span></div>';
html += '<div class="info-row"><span class="label">摄像头</span><span class="value">' + (d.device_id || '-') + '</span></div>';
html += '<div class="info-row"><span class="label">区域</span><span class="value">' + (d.scene_id || '-') + '</span></div>';
html += '<div class="info-row"><span class="label">时间</span><span class="value">' + (d.event_time || '-') + '</span></div>';
html += '</div>';
// VLM 描述
if (d.vlm_description) {
html += '<div class="vlm-desc"><div class="tag">AI 分析</div>' + d.vlm_description + '</div>';
}
// 状态条
html += '<div class="status-bar">';
html += '<div class="status-inner"><span class="status-dot ' + status.dot + '"></span><span class="status-text">' + status.label + '</span></div>';
if (status.sub) html += '<div class="status-sub">' + status.sub + '</div>';
html += '</div>';
// 操作区域(根据状态决定)
if (status.type === 'done') {
html += '<div class="result-bar result-done"><div class="result-text">已处理</div><div class="result-sub">' + (d.handler ? '操作人: ' + d.handler : '') + (d.handle_remark ? ' / ' + d.handle_remark : '') + '</div></div>';
} else if (status.type === 'false') {
html += '<div class="result-bar result-false"><div class="result-text">已忽略(误报)</div><div class="result-sub">' + (d.handler ? '操作人: ' + d.handler : '') + '</div></div>';
} else if (status.type === 'auto') {
html += '<div class="result-bar result-auto"><div class="result-text">已自动关闭</div><div class="result-sub">' + (d.handle_remark || '') + '</div></div>';
} else if (status.type === 'handling') {
// 处理中:可以继续推进到 已处理 或 误报
html += '<div class="actions">';
html += '<button class="btn btn-done" onclick="confirmAction(\'complete\', \'确认已处理?\', \'该告警将标记为已处理并结单\')">已处理</button>';
html += '<button class="btn btn-false" onclick="confirmAction(\'ignore\', \'确认标记误报?\', \'该告警将标记为误报并忽略\')">误报忽略</button>';
html += '</div>';
} else {
// 待处理3 个按钮
html += '<div class="actions">';
html += '<button class="btn btn-go" onclick="confirmAction(\'confirm\', \'前往处理?\', \'告警状态将变为处理中\')">前往处理</button>';
html += '<button class="btn btn-done" onclick="confirmAction(\'complete\', \'确认已处理?\', \'告警将直接标记为已处理并结单\')">已处理</button>';
html += '<button class="btn btn-false" onclick="confirmAction(\'ignore\', \'确认标记误报?\', \'告警将标记为误报并忽略\')">误报忽略</button>';
html += '</div>';
}
document.getElementById('app').innerHTML = html;
}
function confirmAction(action, title, desc) {
pendingAction = action;
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-desc').textContent = desc;
document.getElementById('modal-confirm').onclick = doAction;
document.getElementById('modal').className = 'modal-mask show';
}
function closeModal() {
document.getElementById('modal').className = 'modal-mask';
pendingAction = null;
}
function doAction() {
closeModal();
if (!pendingAction) return;
// 禁用所有按钮
var btns = document.querySelectorAll('.btn');
for (var i = 0; i < btns.length; i++) btns[i].disabled = true;
fetch(baseUrl + '/api/wechat/callback/alarm_action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
alarm_id: alarmId,
action: pendingAction,
operator_uid: 'wechat_user',
remark: null
})
})
.then(function(r) { return r.json(); })
.then(function(json) {
if (json.code !== 0) throw new Error(json.msg || '操作失败');
showToast('操作成功');
setTimeout(loadAlarm, 600);
})
.catch(function(e) {
showToast('操作失败: ' + e.message);
var btns = document.querySelectorAll('.btn');
for (var i = 0; i < btns.length; i++) btns[i].disabled = false;
});
pendingAction = null;
}
function showToast(msg) {
var t = document.getElementById('toast');
t.textContent = msg;
t.style.display = 'block';
setTimeout(function() { t.style.display = 'none'; }, 2000);
}
loadAlarm();
</script>
</body>
</html>