diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx index 5ef067d3..d9c72c3d 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx @@ -63,6 +63,7 @@ test('renders reconciliation queue, loads evidence on demand and records review expect(screen.getByLabelText('运维质量操作')).toHaveClass('v2-workspace-command-bar', 'v2-ops-command-bar'); expect(screen.getByRole('tab', { name: '差异处置', selected: true })).toHaveClass('semi-button'); expect(screen.getByRole('tabpanel', { name: '差异处置' })).toBeInTheDocument(); + expect(document.querySelector('.v2-ops-page')).toHaveClass('is-reconciliation'); expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument(); expect(screen.getByText('数据差异中心').closest('.semi-card')).toHaveClass('v2-reconcile-center'); expect(screen.getByText('数据差异中心').closest('.v2-workspace-panel-header')).toHaveClass('v2-reconcile-heading'); @@ -118,6 +119,7 @@ test('renders only the compact mobile reconciliation surface and defers secondar expect(await screen.findByText('多来源实时位置漂移')).toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).toBeInTheDocument(); + expect(screen.getByRole('region', { name: '差异队列,可上下滚动' })).toHaveAttribute('tabindex', '0'); expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).not.toBeInTheDocument(); expect(mocks.reconciliationIssues).toHaveBeenCalledWith(expect.objectContaining({ limit: 20 }), expect.any(AbortSignal)); const mobileIssueAction = screen.getByRole('button', { name: '查看 粤A00001 差异证据' }); @@ -137,6 +139,7 @@ test('renders only the compact mobile reconciliation surface and defers secondar expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '30 天趋势' })); + expect(await screen.findByRole('dialog', { name: '30 天差异趋势' })).toBeInTheDocument(); const trend = screen.getByText('近 30 天趋势').closest('.v2-reconcile-trend'); expect(trend).toBeInTheDocument(); expect(screen.getByText(/最近运行/)).toBeInTheDocument(); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx index cb90c6b9..c65a9114 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx @@ -339,7 +339,7 @@ export default function OperationsPage() { const capacityIssueCount = data?.capacityFindings.length ?? 0; const healthIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0); const overallStatus = !data ? 'unknown' : healthIssueCount > 0 ? 'warning' : 'ok'; - return
+ return
void; +}) { + return + } + aria-label="收起趋势" + aria-expanded + aria-controls="v2-reconcile-trend" + onClick={onClose} + >收起} + /> +
{data?.trend.slice(-14).map((item) =>
{item.active}
)}
+ {!data?.trend.length ?

完成首次每日检测后显示趋势。

: null} +
存量新增恢复
+
; +} + function ReconciliationDetail({ issue, onClose, sheet = false }: { issue: ReconciliationIssue; onClose: () => void; sheet?: boolean }) { const queryClient = useQueryClient(); const [status, setStatus] = useState(issue.status === 'recovered' ? 'pending' : issue.status); @@ -148,6 +176,7 @@ export default function ReconciliationCenter() { const [trendExpanded, setTrendExpanded] = useState(false); const closeDetail = () => setSelectedID(''); useSideSheetA11y(mobileLayout && Boolean(selectedID), '.v2-reconcile-detail-sidesheet', 'v2-reconcile-detail-sheet', '差异证据与处置', '关闭差异详情'); + useSideSheetA11y(mobileLayout && trendExpanded, '.v2-reconcile-trend-sidesheet', 'v2-reconcile-trend-sheet', '30 天差异趋势', '关闭 30 天差异趋势'); useEffect(() => setOffset(0), [deferredKeyword, mobileLayout, ruleCode, severity, status]); const summary = useQuery({ @@ -266,7 +295,12 @@ export default function ReconciliationCenter() { ]} />
{issues.isError ? issues.refetch()} /> : null} -
+
{!mobileLayout ? item ? detailTriggerRow({ className: selectedID === item.id ? 'is-selected' : '', expanded: selectedID === item.id, @@ -275,7 +309,12 @@ export default function ReconciliationCenter() { onOpen: () => setSelectedID(item.id) }) : ({})} /> :
{issueRows.map((item) => )}
} {issues.isPending ?
: null} @@ -283,27 +322,7 @@ export default function ReconciliationCenter() {
setOffset((next - 1) * pageSize)} />
- {trendExpanded ?
- } - aria-label="收起趋势" - aria-expanded - aria-controls="v2-reconcile-trend" - onClick={() => setTrendExpanded(false)} - >收起} - /> - <>
{data?.trend.slice(-14).map((item) =>
{item.active}
)}
- {!data?.trend.length ?

完成首次每日检测后显示趋势。

: null} -
存量新增恢复
-
: null} + {trendExpanded && !mobileLayout ?
setTrendExpanded(false)} />
: null} {!mobileLayout ? detailPanel : null} @@ -317,5 +336,16 @@ export default function ReconciliationCenter() { > {mobileLayout ? detailPanel : null} + setTrendExpanded(false)} + > + {mobileLayout && trendExpanded ?
setTrendExpanded(false)} />
: null} +
; } diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index 0d0abd89..a1e1b8ae 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -7824,3 +7824,389 @@ overflow: visible; } } + +/* Semi UI operations queue: one fixed workbench with an independently scrolling evidence list. */ +.v2-ops-workspace.is-reconciliation { + display: block; + overflow: hidden; +} + +.v2-ops-workspace.is-reconciliation > .v2-reconcile-center.semi-card { + width: 100%; + height: 100%; + min-height: 0; +} + +.v2-reconcile-center > .semi-card-body { + display: flex; + height: 100%; + min-height: 0; + flex-direction: column; +} + +.v2-reconcile-heading, +.v2-reconcile-overview, +.v2-reconcile-pagination { + flex: 0 0 auto; +} + +.v2-reconcile-layout, +.v2-reconcile-layout.is-trend-open { + height: auto !important; + min-height: 0 !important; + flex: 1 1 0; + overflow: hidden; +} + +.v2-reconcile-main { + min-height: 0; +} + +.v2-reconcile-table-wrap.is-scroll-region { + min-height: 0; + flex: 1 1 0; + overflow: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +.v2-reconcile-table-wrap.is-scroll-region:focus-visible { + outline: 2px solid rgba(18, 104, 243, .34); + outline-offset: -2px; +} + +.v2-reconcile-pagination { + min-height: 48px; +} + +.v2-reconcile-trend-sidesheet .semi-sidesheet-body { + overflow: auto; + background: #f6f8fb; + padding: 10px; +} + +.v2-reconcile-trend-sidesheet .v2-reconcile-trend.semi-card { + overflow: hidden; + border: 1px solid #dce5ef; + border-radius: 12px; + background: #fff; + box-shadow: 0 8px 26px rgba(31, 53, 80, .07); +} + +@media (max-width: 680px) { + .v2-ops-page.is-reconciliation { + height: 100%; + min-height: 0; + overflow: hidden; + padding-bottom: 7px; + } + + .v2-ops-page.is-reconciliation > .v2-ops-command-bar, + .v2-ops-page.is-reconciliation > .v2-ops-tabs { + flex: 0 0 auto; + } + + .v2-ops-page.is-reconciliation > .v2-ops-workspace.is-reconciliation { + display: block; + min-height: 0; + flex: 1 1 0; + overflow: hidden; + } + + .v2-reconcile-heading.v2-workspace-panel-header { + min-height: 54px; + padding: 7px 8px; + } + + .v2-reconcile-heading > .v2-workspace-panel-actions { + gap: 6px; + } + + .v2-reconcile-heading > .v2-workspace-panel-actions > .semi-button { + min-height: 38px; + border-radius: 8px; + font-size: 10px; + } + + .v2-reconcile-overview { + height: 68px; + grid-template-columns: minmax(0, 4fr) minmax(66px, 1fr); + gap: 0; + border-bottom: 1px solid #e3eaf2; + background: #fff; + padding: 0; + } + + .v2-reconcile-overview > .v2-reconcile-kpis { + height: 67px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0; + background: #fff; + } + + .v2-reconcile-overview .v2-reconcile-kpi-card.semi-card { + height: 67px; + border: 0; + border-radius: 0; + box-shadow: none; + } + + .v2-reconcile-overview .v2-reconcile-kpi-card + .v2-reconcile-kpi-card { + border-left: 1px solid #e6ebf2; + } + + .v2-reconcile-overview .v2-reconcile-kpi-card > .semi-card-body { + display: grid; + height: 67px; + min-height: 0; + align-content: center; + gap: 5px; + padding: 6px 4px !important; + text-align: center; + } + + .v2-reconcile-overview .v2-reconcile-kpi-card.is-active { + background: #f2f7ff; + box-shadow: inset 0 -3px #1f6feb; + } + + .v2-reconcile-overview .v2-reconcile-kpi-card small { + overflow: hidden; + color: #75849a; + font-size: 8px; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-reconcile-overview .v2-reconcile-kpi-card strong { + margin: 0; + color: #263b52; + font-size: 17px; + line-height: 1; + } + + .v2-reconcile-overview .v2-reconcile-kpi-card span { + display: none; + } + + .v2-reconcile-sla.semi-card { + height: 67px; + border: 0; + border-left: 1px solid #e6ebf2; + border-radius: 0; + box-shadow: none; + } + + .v2-reconcile-sla > .semi-card-body { + display: grid; + height: 67px; + min-height: 0; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto auto; + align-content: center; + gap: 5px; + padding: 6px 4px !important; + text-align: center; + } + + .v2-reconcile-sla span { + justify-content: center; + } + + .v2-reconcile-sla span small, + .v2-reconcile-sla p { + display: none; + } + + .v2-reconcile-sla .semi-tag { + min-height: 20px; + padding-inline: 6px; + font-size: 8px; + } + + .v2-reconcile-sla strong { + grid-column: 1; + grid-row: 2; + justify-self: center; + font-size: 17px; + } + + .v2-reconcile-layout, + .v2-reconcile-layout.is-trend-open { + display: flex; + min-height: 0; + flex: 1 1 0; + flex-direction: column; + } + + .v2-reconcile-main { + min-height: 0; + flex: 1 1 0; + overflow: hidden; + } + + .v2-reconcile-main > .v2-mobile-filter-toggle { + width: calc(100% - 12px); + flex: 0 0 auto; + margin: 6px; + } + + .v2-reconcile-toolbar { + flex: 0 0 auto; + } + + .v2-reconcile-table-wrap.is-scroll-region { + min-height: 0; + flex: 1 1 0; + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: auto; + touch-action: pan-y; + -webkit-overflow-scrolling: touch; + } + + .v2-reconcile-table-wrap.is-scroll-region::-webkit-scrollbar { + width: 11px; + } + + .v2-reconcile-table-wrap.is-scroll-region::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: #9cacc0; + background-clip: padding-box; + } + + .v2-reconcile-mobile-list { + gap: 6px; + padding: 6px; + } + + .v2-reconcile-mobile-card.semi-card { + border-radius: 9px; + contain-intrinsic-size: auto 118px; + } + + .v2-reconcile-mobile-action.semi-button { + min-height: 116px; + } + + .v2-reconcile-mobile-content { + gap: 5px; + padding: 8px 9px 7px; + } + + .v2-reconcile-mobile-content > header { + min-width: 0; + gap: 6px; + } + + .v2-reconcile-mobile-content > header > strong { + min-width: 0; + overflow: hidden; + color: #243a52; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-reconcile-mobile-content > header > span { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 4px; + } + + .v2-reconcile-mobile-card :is(.v2-reconcile-severity, .v2-reconcile-status).semi-tag { + min-height: 20px; + border-radius: 5px; + padding-inline: 5px; + font-size: 8px; + } + + .v2-reconcile-mobile-content > p { + overflow: hidden; + color: #43576e; + font-size: 10px; + font-weight: 650; + line-height: 15px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-reconcile-mobile-meta { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 8px; + color: #7b899a; + font-size: 8px; + line-height: 12px; + } + + .v2-reconcile-mobile-meta > span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-reconcile-mobile-meta > time { + flex: 0 0 auto; + font-variant-numeric: tabular-nums; + } + + .v2-reconcile-mobile-content > footer { + min-height: 27px; + justify-content: space-between; + gap: 8px; + font-size: 9px; + } + + .v2-reconcile-mobile-content > footer > span { + display: flex; + min-width: 0; + align-items: center; + gap: 3px; + } + + .v2-reconcile-mobile-content > footer > span:first-child { + overflow: hidden; + color: #75859a; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-reconcile-mobile-content > footer > span:last-child { + flex: 0 0 auto; + color: var(--v2-blue); + } + + .v2-reconcile-pagination { + min-height: 50px; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 0; + padding: 5px 6px; + } + + .v2-reconcile-pagination .v2-table-pagination-info { + display: none; + } + + .v2-reconcile-pagination .v2-table-pagination-controls { + width: 100%; + justify-content: center; + } + + .v2-reconcile-pagination .v2-table-pagination-button.semi-button, + .v2-reconcile-pagination .v2-table-pagination-current { + height: 34px; + } + + .v2-reconcile-trend-sidesheet .semi-sidesheet-inner { + width: 100% !important; + } +}