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() {
- {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;
+ }
+}