From 2248eba485ff861d5db8f290d236de662d38beb0 Mon Sep 17 00:00:00 2001
From: QiuSW <105186638@qq.com>
Date: Wed, 12 Aug 2026 11:28:09 +0800
Subject: [PATCH] feat: simplify Bell alert handling for users (#33)
---
prototypes/bell/bell.css | 85 ++++++++++++++++----------
prototypes/bell/bell.js | 78 ++++++++++++++++++++++++
prototypes/bell/index.html | 119 ++++++++++++++++++-------------------
3 files changed, 188 insertions(+), 94 deletions(-)
create mode 100644 prototypes/bell/bell.js
diff --git a/prototypes/bell/bell.css b/prototypes/bell/bell.css
index 30f5062..dfcad35 100644
--- a/prototypes/bell/bell.css
+++ b/prototypes/bell/bell.css
@@ -1,33 +1,54 @@
-/* Bell-only components layered on the shared go-admin-ui prototype adapter. */
-.bell-go-admin .brand-mark { background: #1677ff; }
-.bell-go-admin .bell-workbench { grid-template-columns: minmax(340px,.95fr) minmax(460px,1.35fr); }
-.bell-go-admin .alert-list { display: grid; gap: 8px; }
-.bell-go-admin .alert-row { width: 100%; min-height: 72px; grid-template-columns: 4px minmax(0,1fr) auto; padding: 11px 12px; border-color: #e4e7ed; border-radius: 4px; text-align: left; color: #303133; }
-.bell-go-admin .alert-row:hover { border-color: #91caff; background: #f5f9ff; }
-.bell-go-admin .alert-row.selected { border-color: #69b1ff; background: #e6f4ff; box-shadow: inset 0 0 0 1px #91caff; }
-.bell-go-admin .severity-bar { background: #f56c6c; }
-.bell-go-admin .severity-bar.medium { background: #e6a23c; }
-.bell-go-admin .alert-title { display: block; font-size: 14px; font-weight: 500; }
-.bell-go-admin .alert-meta { margin-top: 5px; }
-.bell-go-admin .alert-actions { margin-top: 14px; display: flex; justify-content: flex-end; gap: 8px; }
-.bell-go-admin .el-tabs { margin-bottom: 14px; }
-.bell-go-admin .el-tabs button { height: 36px; padding: 0 16px; color: #606266; font-size: 13px; }
-.bell-go-admin .el-tabs button[aria-selected="true"] { color: #1677ff; border-bottom-color: #1677ff; }
-.bell-go-admin .timeline { padding-left: 2px; }
-.bell-go-admin .timeline-item::before { background: #1677ff; box-shadow: 0 0 0 4px #e6f4ff; }
-.bell-go-admin .dev-only-note { margin: 0 0 18px; border-left-color: #e6a23c; border-radius: 4px; }
-
-@media (max-width: 1080px) {
- .bell-go-admin .bell-workbench { grid-template-columns: 1fr; }
-}
-
-@media (max-width: 900px) {
- .bell-go-admin .alert-row { min-height: 76px; }
- .bell-go-admin .alert-actions { flex-wrap: wrap; }
- .bell-go-admin .alert-actions .el-button { flex: 1 1 150px; }
-}
-
-@media (max-width: 430px) {
- .bell-go-admin .alert-row { grid-template-columns: 4px minmax(0,1fr); }
- .bell-go-admin .alert-row .activity-time { grid-column: 2; }
+.bell-go-admin .sidebar { background: linear-gradient(180deg, #7c2d12 0%, #9a3412 44%, #431407 100%); }
+.bell-go-admin .brand-mark { background: #ffedd5; color: #9a3412; }
+.bell-go-admin .el-menu-item.active { background: rgba(255, 255, 255, 0.18); }
+.bell-go-admin .nav-count { margin-left: auto; min-width: 20px; padding: 1px 6px; border-radius: 10px; background: rgba(255,255,255,.2); font-size: 12px; text-align: center; }
+.bell-go-admin .compact-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.bell-workbench { grid-template-columns: minmax(290px, .8fr) minmax(440px, 1.2fr); align-items: start; }
+.alert-workbench .card-body { padding: 0; }
+.alert-list { display: grid; }
+.alert-row { display: grid; grid-template-columns: 4px minmax(0, 1fr) auto; gap: 14px; width: 100%; min-height: 82px; padding: 15px 18px; border: 0; border-bottom: 1px solid var(--border); background: #fff; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
+.alert-row:hover { background: #fff7ed; }
+.alert-row:focus-visible { outline: 2px solid #c2410c; outline-offset: -3px; }
+.alert-row.selected { background: #fff7ed; box-shadow: inset 3px 0 #c2410c; }
+.severity-bar { width: 4px; height: 48px; border-radius: 4px; background: var(--danger); }
+.severity-bar.medium { background: var(--warning); }
+.alert-title { display: block; margin-bottom: 7px; }
+.alert-meta { display: flex; flex-wrap: wrap; gap: 6px 12px; color: var(--text-muted); font-size: 13px; }
+.activity-time { color: var(--text-muted); font-size: 13px; }
+.situation-summary { display: grid; gap: 12px; }
+.situation-summary > div { display: grid; grid-template-columns: 92px 1fr; gap: 14px; }
+.situation-summary span { color: var(--text-muted); }
+.evidence-preview { display: flex; align-items: flex-end; justify-content: space-between; min-height: 170px; margin: 18px 0; padding: 18px; border-radius: 8px; background: linear-gradient(140deg, #172033, #506078); color: #fff; }
+.evidence-preview span { color: #e2e8f0; }
+.quick-actions { display: flex; flex-wrap: wrap; gap: 10px; }
+.primary-task { margin-top: 18px; padding: 15px; border-left: 4px solid #c2410c; border-radius: 4px; background: #fff7ed; }
+.primary-task p { margin: 0 0 12px; line-height: 1.6; }
+.technical-details { margin-top: 18px; border: 1px solid var(--border); border-radius: 6px; background: #f8fafc; }
+.technical-details summary { padding: 11px 14px; cursor: pointer; color: #475569; font-weight: 600; }
+.technical-details summary:focus-visible { outline: 2px solid #c2410c; outline-offset: 2px; }
+.technical-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; padding: 0 14px 14px; }
+.technical-grid > div { display: grid; gap: 4px; }
+.technical-grid span { color: var(--text-muted); font-size: 13px; }
+.technical-grid strong { overflow-wrap: anywhere; }
+.working-list { display: grid; gap: 10px; }
+.working-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px; border: 1px solid var(--border); border-radius: 8px; }
+.working-item p { margin: 5px 0 0; color: var(--text-muted); }
+.result-options { display: grid; gap: 10px; margin: 0; padding: 0; border: 0; }
+.result-options legend { margin-bottom: 4px; font-weight: 700; }
+.result-card { display: flex; gap: 12px; min-height: 64px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; }
+.result-card:has(input:checked) { border-color: #c2410c; background: #fff7ed; box-shadow: 0 0 0 1px #c2410c; }
+.result-card input { margin-top: 4px; }
+.result-card span { display: grid; gap: 3px; }
+.result-card small { color: var(--text-muted); }
+.field-error { color: #b91c1c; font-size: 13px; }
+.dialog-subtitle { margin: 4px 0 0; color: var(--text-muted); font-size: 13px; }
+.admin-records { margin-bottom: 14px; }
+@media (max-width: 1100px) { .bell-workbench { grid-template-columns: 1fr; } }
+@media (max-width: 767px) {
+ .bell-go-admin .compact-metrics { grid-template-columns: 1fr; }
+ .situation-summary > div { grid-template-columns: 1fr; gap: 3px; }
+ .technical-grid { grid-template-columns: 1fr; }
+ .working-item { align-items: stretch; flex-direction: column; }
+ .working-item .btn { width: 100%; }
+ .evidence-preview { align-items: flex-start; flex-direction: column; }
}
diff --git a/prototypes/bell/bell.js b/prototypes/bell/bell.js
new file mode 100644
index 0000000..7c6a09d
--- /dev/null
+++ b/prototypes/bell/bell.js
@@ -0,0 +1,78 @@
+(function () {
+ "use strict";
+
+ const qs = (selector, root = document) => root.querySelector(selector);
+ const qsa = (selector, root = document) => [...root.querySelectorAll(selector)];
+ const toastRegion = qs("[data-toast-region]");
+ const toast = (message) => {
+ if (!toastRegion) return;
+ const item = document.createElement("div");
+ item.className = "toast";
+ item.setAttribute("role", "status");
+ item.textContent = message;
+ toastRegion.appendChild(item);
+ window.setTimeout(() => item.remove(), 3600);
+ };
+ const number = (selector) => Number(qs(selector)?.textContent || 0);
+
+ qsa("[data-select-alert]").forEach((row) => row.addEventListener("click", () => {
+ qs("[data-detail-title]").textContent = row.dataset.selectAlert;
+ qs("[data-detail-what]").textContent = row.dataset.selectAlert;
+ qs("[data-detail-location]").textContent = row.dataset.location;
+ qs("[data-detail-meta]").textContent = `${row.dataset.location} · ${row.dataset.time}`;
+ qs("[data-detail-suggestion]").textContent = row.dataset.suggestion;
+ const state = qs("[data-alert-state]");
+ state.textContent = row.dataset.risk;
+ state.className = `status ${row.dataset.risk === "高风险" ? "status-danger" : "status-warning"} el-tag`;
+ }));
+
+ let started = false;
+ const startButton = qs("[data-action='start-alert']");
+ startButton?.addEventListener("click", () => {
+ if (started) return;
+ started = true;
+ startButton.disabled = true;
+ startButton.textContent = "已由你开始处理";
+ const state = qs("[data-alert-state]");
+ state.textContent = "处理中";
+ state.className = "status status-warning el-tag";
+ qs("[data-pending-count]").textContent = String(Math.max(0, number("[data-pending-count]") - 1));
+ qs("[data-working-count]").textContent = String(number("[data-working-count]") + 1);
+ qs("[data-owned-alert]").hidden = false;
+ qs("[data-working-total]").textContent = "3 条";
+ toast("已开始处理;其他人会看到这条预警由王老师负责");
+ });
+
+ qs("[data-simulate-conflict]")?.addEventListener("click", () => {
+ toast("这条预警已由李老师开始处理,你可以查看进展但不能重复领取");
+ });
+
+ qsa("input[name='result']").forEach((input) => input.addEventListener("change", () => {
+ qs("[data-result-error]").hidden = true;
+ }));
+
+ qs("[data-action='finish-alert']")?.addEventListener("click", () => {
+ const selected = qs("input[name='result']:checked");
+ const error = qs("[data-result-error]");
+ if (!selected) {
+ error.hidden = false;
+ qs("input[name='result']")?.focus();
+ return;
+ }
+ const note = qs("#result-note").value.trim();
+ const tbody = qs("[data-history-body]");
+ const row = document.createElement("tr");
+ const statusClass = selected.value === "确认有危险" ? "status-danger" : selected.value === "现场正常" ? "status-success" : "status-neutral";
+ row.innerHTML = `
危险区域有人进入 | 北围墙 01 | | 王老师 | 刚刚 | | `;
+ qs(".table-subtitle", row).textContent = note || "未填写补充说明";
+ qs(".status", row).textContent = selected.value;
+ qs("button", row).addEventListener("click", () => toast("已打开完整处理记录"));
+ tbody.prepend(row);
+ qs("[data-owned-alert]").hidden = true;
+ qs("[data-working-total]").textContent = "2 条";
+ qs("[data-working-count]").textContent = String(Math.max(0, number("[data-working-count]") - 1));
+ qs("[data-completed-count]").textContent = String(number("[data-completed-count]") + 1);
+ qs("[data-close-modal]", qs("#finish-alert"))?.click();
+ toast(`处理已完成,现场结果:${selected.value}`);
+ });
+})();
diff --git a/prototypes/bell/index.html b/prototypes/bell/index.html
index 6994def..dbbae21 100644
--- a/prototypes/bell/index.html
+++ b/prototypes/bell/index.html
@@ -3,8 +3,8 @@
-
- YoVision Bell · go-admin-ui 对齐原型
+
+ YoVision Bell · 预警处理
@@ -14,31 +14,26 @@
跳到主要内容
@@ -47,82 +42,82 @@
-
-
-
-
待确认 Alert
3
-
处理中
2
-
今日关闭
18
-
重复收据
6
+
+
+
-
-
-
-
+
+
+
+
+
+
+
发生了什么危险区域有人进入
发生地点北围墙 01
建议怎么做先查看现场;如无法确认,请联系校内保安巡查。
+
北围墙 01 · 10:23:55查看现场画面
+
+
准备处理这条预警时,请点击下面的按钮。此操作只表示“你已看到并开始处理”,不代表现场情况已确认。
+
管理员技术详情
预警编号BEL-A-20260812-0031
原始事实2 条不可变事件
匹配规则校园高危区域 · v4
后台状态映射Alert new → ack → close
来源标识producer-lab / evt-000042
-
-
-
规则校园高危区域 · v4
处置建议确认现场情况;如无法确认,联系校内保安巡查。
-
producer-lab / evt-000041
不可变 Event · 10:23:48 · 证据引用可用
producer-lab / evt-000042
不可变 Event · 10:23:55 · 同一 Alert 聚合
-
规则命中并创建 Alert
10:23:56 · system · 规则 v4
值班台已显示
10:23:57 · sent/seen 与 ack 分离
-
-
-
-
-
操作按钮受 RBAC 与环境开关双重控制
-
| Event | Producer / Source ID | 接收时间 | 收据 | Alert | 操作 |
- | 危险区域进入结构化事实已冻结 | producer-lab / evt-000042 | 10:23:55 | 首次接收 | BEL-A-0031 | |
- | 危险区域进入正文与首次一致 | producer-lab / evt-000042 | 10:24:03 | 重复收据 | 未重复创建 | |
- | 方向越线结构化事实已冻结 | producer-lab / evt-000039 | 10:19:12 | 首次接收 | BEL-A-0029 | |
-
-
+
+
+
+
危险区域有人进入北围墙 01 · 由王老师处理 · 刚刚开始
+
有人反向越过警戒线宿舍东门 02 · 由李老师处理 · 5 分钟前开始
+
夜间区域有人停留操场西侧 · 由保安室处理 · 8 分钟前开始
-
-
-
-
| 规则名称 | 发布版本 | 匹配条件 | Alert 严重度 | 状态 | 操作 |
|---|
| 校园高危区域 | v4 | event.type = zone_intrusion | 高危 | 生效 | |
| 方向越线 | v2 | event.type = line_crossing | 中危 | 生效 | |
+
+
+
+
+
| 发生情况 | 地点 | 处理结果 | 处理人 | 完成时间 | 操作 |
+ | 有人进入危险区域 | 教学楼楼顶 | 确认有危险 | 赵老师 | 09:42 | |
+ | 有人反向越过警戒线 | 宿舍西门 | 误报 | 王老师 | 09:18 | |
+ | 危险区域有人停留 | 食堂后门 | 现场正常 | 李老师 | 08:57 | |
+
+
管理员:查看技术事件和审计链路
不可变事件原始内容保持不变
重复接收保护6 条重复收据,均未重复创建预警
规则管理仅管理员可配置发布版本
开发测试入口生产环境不显示合成事件注入
+
-
-
-
- 该入口仅用于开发测试;生产构建必须通过环境开关移除。
-
+
+