Compare commits

..

2 Commits

Author SHA1 Message Date
lingniu
270bb163eb fix(web): clarify cumulative mileage evidence 2026-07-20 05:41:45 +08:00
lingniu
2c09fd7cf0 feat(web): refine alert notification workspace 2026-07-20 05:41:45 +08:00
6 changed files with 96 additions and 7 deletions

View File

@@ -60,6 +60,17 @@ stat-writer 已支持在累计里程缺失时,用有速度证据的相邻 GPS
03:56 新版本 `mileage-gps-candidate-reconcile-202607200350` 完成清理型正式回补:写入 452 个来源聚合,`subthresholdGPSCandidatesFound=1``subthresholdGPSCandidatesDeleted=1`。沪A56988F 当天阈值下候选及错误最终投影均已删除;剩余被选中的 GPS 候选最小值为 `0.131 km`,低于 `0.1 km` 的选中候选为 0。清理后该车显式暴露为“JT808 当日缺少新鲜总里程字段”的源数据缺口,统计管道缺口仍为 0系统保留该缺口不用 4 米静止漂移构造业务里程。
## 7 月 20 日 05:40 再次复核
针对“轨迹回放有里程、今日里程没有计算”的再次反馈,按 `2026-07-20 00:00:00``05:40:00` 重新核对生产轨迹、实时总里程证据、来源候选和最终日里程:
| 车牌 | 今日轨迹证据 | 今日统计 | 处理结论 |
| --- | --- | --- | --- |
| 沪A01559F | 宇通 260 个源点、259 个有效点;最高/平均速度均为 0坐标区间累计 0 km | 宇通 `stationary_carry_forward` 0 km仪表累计总里程 14,097 km 的最新证据仍是 7 月 19 日 21:23:59 | 已有显式 0 km 统计,无可信正向里程可补 |
| 沪A62013F | 今日 0 个源点7 月 19 日 520 个源点最高速度仍为 0区间里程 0 km | 7 月 19 日宇通 0 km累计总里程 24,245 km | 今日没有上报,昨日 0 km 正确,禁止把累计值补成日里程 |
同一时点全量诊断覆盖 480 个活跃“协议 × 车辆”项,`pipeline_issue_total=0`;仅有 1 个赛格 JT808 项缺少新鲜总里程,属于上游源数据问题,与这两辆车无关。轨迹页点位卡片同步将“累计里程”明确为“累计总里程”,并说明它不是所选时间窗里程或今日里程,避免把点位累计值误读为区间结果。
## 限制与后续监控
- GPS 估算只在仪表里程缺失或停滞时兜底,不替代 GB32960、宇通仪表里程或 JT808 设备里程。

View File

@@ -150,7 +150,7 @@ test('clears old alert rows and inspector evidence when the event scope changes'
expect(screen.getByLabelText('告警中心工作区').querySelector('.v2-workspace-command-icon.semi-avatar')).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 5, name: '告警处置工作台' })).toBeInTheDocument();
expect(screen.getByText('核验事件证据、治理规则并追踪通知状态')).toBeInTheDocument();
expect(screen.getByText('可处置事件')).toBeInTheDocument();
expect(screen.getByText('具备处置权限')).toBeInTheDocument();
expect(screen.queryByRole('heading', { level: 2, name: '告警中心' })).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-workspace')).not.toHaveClass('is-inspector-open');
@@ -326,7 +326,7 @@ test('uses one focused Semi bottom SideSheet for the complete mobile event filte
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts']}><AlertsPage /></MemoryRouter></QueryClientProvider>);
await screen.findByText('当前筛选条件没有告警事件');
const trigger = screen.getByRole('button', { name: /修改事件筛选/ });
const trigger = screen.getByRole('button', { name: '打开告警筛选:全部告警' });
expect(trigger).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(trigger);
@@ -346,7 +346,7 @@ test('uses one focused Semi bottom SideSheet for the complete mobile event filte
fireEvent.click(within(dialog).getByRole('button', { name: '应用并查询' }));
await waitFor(() => expect(mocks.alertEventsV2).toHaveBeenLastCalledWith(expect.objectContaining({ protocol: 'JT808' }), expect.anything()));
expect(screen.getByRole('button', { name: /修改事件筛选:已启用 1 项/ })).toHaveAttribute('aria-expanded', 'false');
expect(screen.getByRole('button', { name: '打开告警筛选:已启用 1 项' })).toHaveAttribute('aria-expanded', 'false');
});
test('keeps mobile rule selection focused and opens editing in a Semi bottom SideSheet', async () => {

View File

@@ -284,12 +284,13 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
return <>
{mobileLayout ? <div className="v2-alert-mobile-discovery">
<MobileFilterToggle
title="事件筛选"
title="筛选告警"
summary={activeFilterCount ? `已启用 ${activeFilterCount}` : '全部告警'}
expanded={mobileFiltersOpen}
controls="v2-alert-mobile-filters"
expandedLabel="关闭"
collapsedLabel="修改"
collapsedLabel="筛选"
ariaLabel={`${mobileFiltersOpen ? '关闭' : '打开'}告警筛选:${activeFilterCount ? `已启用 ${activeFilterCount}` : '全部告警'}`}
onToggle={() => setFiltersCollapsed((value) => !value)}
/>
<MobileFilterSheet
@@ -640,7 +641,7 @@ export default function AlertsPage() {
description="核验事件证据、治理规则并追踪通知状态"
status={unread ? `${unread.toLocaleString('zh-CN')} 条未读` : '通知已读'}
statusColor={unread ? 'orange' : 'green'}
meta={<Typography.Text type="tertiary">{operator ? '可处置事件' : '只读查看'}</Typography.Text>}
meta={<Typography.Text type="tertiary">{operator ? '具备处置权限' : '仅限查看'}</Typography.Text>}
actions={<SegmentedTabs className="v2-alert-tabs" variant="filled" ariaLabel="告警中心分类" value={activeTab} items={tabs} onChange={setTab} />}
/>
{activeTab !== 'notifications' && rules.isError ? <InlineError message={rules.error.message} onRetry={() => rules.refetch()} /> : null}

View File

@@ -197,6 +197,8 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
expect(view.container.querySelector('.v2-track-current-card.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-current-coverage')).toBeInTheDocument();
expect(view.container.querySelectorAll('.v2-track-current-metrics > span')).toHaveLength(4);
expect(view.container.querySelector('.v2-track-current-metrics')).toHaveTextContent('累计总里程100 km');
expect(view.container.querySelector('.v2-track-current-metrics > span:last-child')).toHaveAttribute('title', '当前轨迹点设备上报的累计总里程,不是所选时间窗里程或今日里程');
expect(view.container.querySelector('.v2-track-current-evidence')).toHaveTextContent('JT/T 808时间窗完整3 → 3 点报警 —');
expect(view.container.querySelector('.v2-track-current-evidence .v2-track-protocol-tag')).toHaveClass('semi-tag-cyan-light');
expect(view.container.querySelector('.v2-track-coverage-float')).not.toBeInTheDocument();

View File

@@ -515,7 +515,7 @@ export default function TrackPage() {
<span><small></small><strong>{number(current?.speedKmh ?? 0)}<em> km/h</em></strong></span>
<span><small></small><strong>{direction(current?.directionDeg)}</strong></span>
<span><small>SOC</small><strong>{current?.socAvailable ? `${number(current.socPercent)}%` : '—'}</strong></span>
<span><small></small><strong>{trackPointMileageAvailable(current) ? number(current?.totalMileageKm ?? 0) : '—'}{trackPointMileageAvailable(current) ? <em> km</em> : null}</strong></span>
<span title="当前轨迹点设备上报的累计总里程,不是所选时间窗里程或今日里程"><small></small><strong>{trackPointMileageAvailable(current) ? number(current?.totalMileageKm ?? 0) : '—'}{trackPointMileageAvailable(current) ? <em> km</em> : null}</strong></span>
</div>
<div className={`v2-track-current-evidence v2-track-current-coverage${track.coverage.complete ? '' : ' is-warning'}`}>
<ProtocolTag className="v2-track-protocol-tag" protocol={current?.protocol} compact />

View File

@@ -9431,6 +9431,81 @@
padding-inline: 7px;
}
@media (min-width: 681px) {
.v2-alert-notification-list {
padding: 0 10px 10px;
}
.v2-alert-notification-cards.semi-card-group {
overflow: hidden;
grid-template-columns: minmax(0, 1fr);
gap: 0;
border: 1px solid #e1e8f0;
border-radius: 10px;
background: #fff;
}
.v2-alert-notification-card.semi-card {
display: grid;
min-height: 64px;
grid-template-columns: minmax(248px, .72fr) minmax(0, 1.7fr);
border: 0;
border-bottom: 1px solid #e8edf3;
border-radius: 0;
box-shadow: none;
contain-intrinsic-size: auto 64px;
transition: background-color .16s ease;
}
.v2-alert-notification-card.semi-card:last-child {
border-bottom: 0;
}
.v2-alert-notification-card.semi-card:hover {
background: #f8fbff;
}
.v2-alert-notification-card.is-unread.semi-card {
box-shadow: inset 3px 0 #2f78e8;
}
.v2-alert-notification-card > .semi-card-header {
min-height: 64px;
border-right: 1px solid #edf1f5;
border-bottom: 0;
padding: 8px 12px;
}
.v2-alert-notification-card > .semi-card-body {
display: grid;
min-height: 64px;
grid-template-columns: minmax(0, 1fr) 138px;
}
.v2-alert-notification-content {
display: -webkit-box;
min-height: 64px;
overflow: hidden;
align-content: center;
margin: 0;
padding: 12px 14px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.v2-alert-notification-card > .semi-card-body > footer {
min-height: 64px;
justify-content: center;
align-items: flex-end;
flex-direction: column;
gap: 2px;
border-top: 0;
border-left: 1px solid #edf1f5;
padding: 7px 9px;
}
}
.v2-alert-notification-pagination {
display: flex;
min-height: 48px;