feat: 重写H5告警详情页,实现状态感知的交互设计
- 待处理: 显示3个按钮(前往处理/已处理/误报忽略) - 处理中: 显示2个按钮(已处理/误报忽略) - 已处理/已忽略/自动关闭: 隐藏按钮,显示结果条 - 添加确认弹窗防止误操作 - 优化移动端显示(word-break, 圆角等) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica
|
||||
|
||||
/* 告警信息 */
|
||||
.info-card { background: #fff; border-radius: 8px; padding: 16px; margin-bottom: 12px; }
|
||||
.info-card .level-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; color: #fff; margin-bottom: 8px; }
|
||||
.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; }
|
||||
@@ -25,162 +25,227 @@ body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica
|
||||
.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; }
|
||||
.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: 12px 16px; margin-bottom: 12px; text-align: center; }
|
||||
.status-bar .status { font-size: 14px; font-weight: 500; }
|
||||
.status-new { color: #fa541c; }
|
||||
.status-handling { color: #faad14; }
|
||||
.status-done { color: #52c41a; }
|
||||
.status-false { color: #999; }
|
||||
/* 状态条 */
|
||||
.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; }
|
||||
.actions .btn { flex: 1; padding: 12px 0; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: opacity 0.2s; }
|
||||
.actions .btn:active { opacity: 0.7; }
|
||||
.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: #f0f0f0; color: #666; }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.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; }
|
||||
|
||||
/* 已操作提示 */
|
||||
.done-msg { text-align: center; padding: 16px; background: #f6ffed; border-radius: 8px; margin-bottom: 12px; color: #52c41a; font-size: 14px; }
|
||||
.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="snapshot" id="snapshot-area"></div>
|
||||
<div class="info-card" id="info-card"></div>
|
||||
<div id="vlm-area"></div>
|
||||
<div class="status-bar" id="status-bar"></div>
|
||||
<div id="action-area"></div>
|
||||
<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>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const alarmId = params.get('alarm_id');
|
||||
const baseUrl = location.origin;
|
||||
var params = new URLSearchParams(location.search);
|
||||
var alarmId = params.get('alarm_id');
|
||||
var baseUrl = location.origin;
|
||||
var alarmData = null;
|
||||
var pendingAction = null;
|
||||
|
||||
// 告警类型映射
|
||||
const typeNames = { leave_post: '人员离岗', intrusion: '周界入侵' };
|
||||
const levelNames = { 1: '提醒', 2: '一般', 3: '严重', 4: '紧急' };
|
||||
const statusNames = { NEW: '待处理', CONFIRMED: '已确认', HANDLING: '处理中', CLOSED: '已结单', FALSE: '误报', RESOLVED: '自动结束' };
|
||||
const handleNames = { UNHANDLED: '未处理', HANDLING: '处理中', DONE: '已完成' };
|
||||
var typeNames = { leave_post: '人员离岗', intrusion: '周界入侵' };
|
||||
var levelNames = { 1: '提醒', 2: '一般', 3: '严重', 4: '紧急' };
|
||||
|
||||
let alarmData = null;
|
||||
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' };
|
||||
}
|
||||
|
||||
async function loadAlarm() {
|
||||
function loadAlarm() {
|
||||
if (!alarmId) {
|
||||
document.getElementById('app').innerHTML = '<div style="text-align:center;padding:60px 0;color:#999;">缺少告警ID参数</div>';
|
||||
document.getElementById('app').innerHTML = '<div class="loading">缺少告警ID参数</div>';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(baseUrl + '/api/wechat/alarm_detail?alarm_id=' + encodeURIComponent(alarmId));
|
||||
const json = await resp.json();
|
||||
if (json.code !== 0) throw new Error(json.msg || '加载失败');
|
||||
alarmData = json.data;
|
||||
render();
|
||||
} catch (e) {
|
||||
document.getElementById('app').innerHTML = '<div style="text-align:center;padding:60px 0;color:#f00;">加载失败: ' + e.message + '</div>';
|
||||
}
|
||||
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() {
|
||||
const d = alarmData;
|
||||
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 = '';
|
||||
|
||||
// 截图
|
||||
const snapArea = document.getElementById('snapshot-area');
|
||||
html += '<div class="snapshot">';
|
||||
if (d.snapshot_url) {
|
||||
snapArea.innerHTML = '<img src="' + d.snapshot_url + '" alt="告警截图" onerror="this.parentElement.innerHTML=\'<div class=no-img>截图加载失败</div>\'">';
|
||||
html += '<img src="' + d.snapshot_url + '" alt="告警截图" onerror="this.parentElement.innerHTML=\'<div class=no-img>截图加载失败</div>\'">';
|
||||
} else {
|
||||
snapArea.innerHTML = '<div class="no-img">暂无截图</div>';
|
||||
html += '<div class="no-img">暂无截图</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// 信息卡片
|
||||
const typeName = typeNames[d.alarm_type] || d.alarm_type;
|
||||
const levelName = levelNames[d.alarm_level] || '一般';
|
||||
const levelClass = 'level-' + (d.alarm_level || 2);
|
||||
|
||||
document.getElementById('info-card').innerHTML =
|
||||
'<span class="level-badge ' + levelClass + '">' + levelName + '</span>' +
|
||||
'<div class="title">' + typeName + '告警</div>' +
|
||||
'<div class="info-row"><span class="label">告警ID</span><span class="value">' + (d.alarm_id || '').slice(-12) + '</span></div>' +
|
||||
'<div class="info-row"><span class="label">摄像头</span><span class="value">' + (d.device_id || '-') + '</span></div>' +
|
||||
'<div class="info-row"><span class="label">区域</span><span class="value">' + (d.scene_id || '-') + '</span></div>' +
|
||||
'<div class="info-row"><span class="label">时间</span><span class="value">' + (d.event_time || '-') + '</span></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 描述
|
||||
const vlmArea = document.getElementById('vlm-area');
|
||||
if (d.vlm_description) {
|
||||
vlmArea.innerHTML = '<div class="vlm-desc"><div class="tag">AI 分析</div>' + d.vlm_description + '</div>';
|
||||
} else {
|
||||
vlmArea.innerHTML = '';
|
||||
html += '<div class="vlm-desc"><div class="tag">AI 分析</div>' + d.vlm_description + '</div>';
|
||||
}
|
||||
|
||||
// 状态
|
||||
const alarmStatus = d.alarm_status || 'NEW';
|
||||
const handleStatus = d.handle_status || 'UNHANDLED';
|
||||
const statusClass = alarmStatus === 'FALSE' ? 'status-false' : handleStatus === 'DONE' ? 'status-done' : handleStatus === 'HANDLING' ? 'status-handling' : 'status-new';
|
||||
document.getElementById('status-bar').innerHTML =
|
||||
'<div class="status ' + statusClass + '">' + (statusNames[alarmStatus] || alarmStatus) + ' / ' + (handleNames[handleStatus] || handleStatus) + '</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>';
|
||||
|
||||
// 操作按钮(已处理完的不显示)
|
||||
const actionArea = document.getElementById('action-area');
|
||||
if (handleStatus === 'DONE' || alarmStatus === 'FALSE' || alarmStatus === 'CLOSED' || alarmStatus === 'RESOLVED') {
|
||||
const handler = d.handler || '';
|
||||
const remark = d.handle_remark || '';
|
||||
actionArea.innerHTML = '<div class="done-msg">' + (remark || '已处理') + (handler ? '<br><span style="font-size:12px;color:#999;">操作人: ' + handler + '</span>' : '') + '</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 {
|
||||
actionArea.innerHTML =
|
||||
'<div class="actions">' +
|
||||
'<button class="btn btn-go" onclick="doAction(\'confirm\')">前往处理</button>' +
|
||||
'<button class="btn btn-done" onclick="doAction(\'complete\')">已处理</button>' +
|
||||
'<button class="btn btn-false" onclick="doAction(\'ignore\')">误报忽略</button>' +
|
||||
'</div>';
|
||||
// 待处理: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;
|
||||
}
|
||||
|
||||
async function doAction(action) {
|
||||
const actionNames = { confirm: '前往处理', complete: '已处理', ignore: '误报忽略' };
|
||||
if (!confirm('确定执行【' + actionNames[action] + '】?')) return;
|
||||
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;
|
||||
|
||||
// 禁用所有按钮
|
||||
document.querySelectorAll('.btn').forEach(b => b.disabled = true);
|
||||
var btns = document.querySelectorAll('.btn');
|
||||
for (var i = 0; i < btns.length; i++) btns[i].disabled = true;
|
||||
|
||||
try {
|
||||
const resp = await fetch(baseUrl + '/api/wechat/callback/alarm_action', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
alarm_id: alarmId,
|
||||
action: action,
|
||||
operator_uid: 'wechat_user',
|
||||
remark: null,
|
||||
}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
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, 800);
|
||||
} catch (e) {
|
||||
setTimeout(loadAlarm, 600);
|
||||
})
|
||||
.catch(function(e) {
|
||||
showToast('操作失败: ' + e.message);
|
||||
document.querySelectorAll('.btn').forEach(b => b.disabled = false);
|
||||
}
|
||||
var btns = document.querySelectorAll('.btn');
|
||||
for (var i = 0; i < btns.length; i++) btns[i].disabled = false;
|
||||
});
|
||||
|
||||
pendingAction = null;
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
var t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.style.display = 'block';
|
||||
setTimeout(() => t.style.display = 'none', 2000);
|
||||
setTimeout(function() { t.style.display = 'none'; }, 2000);
|
||||
}
|
||||
|
||||
loadAlarm();
|
||||
|
||||
Reference in New Issue
Block a user