Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx
2026-07-19 05:16:55 +08:00

551 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons';
import { Button, Card, CardGroup, Checkbox, Descriptions, Empty, Input, Progress, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { VehicleCoverageRow, VehicleLocationSourceEvidence, VehicleSourceDiagnostic } from '../../api/types';
import { InlineError } from '../shared/AsyncState';
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
import { PlatformTime } from '../shared/PlatformTime';
import { SegmentedTabs } from '../shared/SegmentedTabs';
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
import { useMobileLayout } from '../hooks/useMobileLayout';
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
import ReconciliationCenter from './ReconciliationCenter';
type OperationsWorkspace = 'reconciliation' | 'diagnostic' | 'health';
const operationsWorkspaces = new Set<OperationsWorkspace>(['reconciliation', 'diagnostic', 'health']);
function statusLabel(status: string) {
return { ok: '正常', warning: '关注', error: '异常' }[status] ?? status;
}
const opsLinkLabels: Record<string, string> = {
'MySQL realtime': 'MySQL 实时连接',
vehicle_realtime_snapshot: '车辆实时快照',
vehicle_realtime_location: '车辆实时位置',
'TDengine raw_frames': 'TDengine 原始帧',
'Capacity check': '容量与消费积压',
'Redis online keys': 'Redis 在线状态',
'Alert Kafka stream': '告警消费链路'
};
function opsLinkLabel(name: string) {
return opsLinkLabels[name] ?? name;
}
function HealthTag({ status, children }: { status: string; children?: string }) {
const color = status === 'ok' ? 'green' : status === 'warning' ? 'orange' : status === 'error' ? 'red' : 'grey';
return <Tag className={`v2-ops-health-tag is-${status}`} color={color} type="light" size="small"><i />{children ?? statusLabel(status)}</Tag>;
}
function number(value?: number, digits = 1) {
return value == null || !Number.isFinite(value) ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits });
}
function OpsMetric({ label, value, detail, tone = 'neutral' }: {
label: string;
value: string;
detail: string;
tone?: 'neutral' | 'success' | 'warning' | 'danger';
}) {
return <article className={`v2-ops-metric is-${tone}`} role="listitem">
<small>{label}</small>
<strong>{value}</strong>
<span>{detail}</span>
</article>;
}
function sourceOnlineRate(online: number, total: number, onlineRate?: number) {
if (onlineRate != null && Number.isFinite(onlineRate)) return Math.max(0, Math.min(100, onlineRate));
return total > 0 ? Math.max(0, Math.min(100, (online / total) * 100)) : 0;
}
function SourcePolicyEditor({ vin, source, diagnostic, editable, onSaved }: {
vin: string;
source: VehicleLocationSourceEvidence;
diagnostic: VehicleSourceDiagnostic;
editable: boolean;
onSaved: (next: VehicleSourceDiagnostic) => void;
}) {
const [enabled, setEnabled] = useState(source.enabled);
const [priority, setPriority] = useState(source.priority);
const [providerName, setProviderName] = useState(source.providerOverride || '');
const [providerEvidence, setProviderEvidence] = useState('');
const [remark, setRemark] = useState(source.policyRemark || '');
useEffect(() => {
setEnabled(source.enabled);
setPriority(source.priority);
setProviderName(source.providerOverride || '');
setProviderEvidence('');
setRemark(source.policyRemark || '');
}, [source.enabled, source.priority, source.providerOverride, source.policyRemark, source.sourceRef]);
const save = useMutation({
mutationFn: () => api.updateVehicleSourcePolicy(vin, {
version: diagnostic.policy.version,
sourceRef: source.sourceRef ?? '',
providerName,
providerEvidence,
enabled,
priority,
remark
}),
onSuccess: onSaved
});
const providerChanged = providerName.trim() !== (source.providerOverride || '');
const policyEditable = source.sourceKind !== 'CANONICAL';
const policyChanged = enabled !== source.enabled || priority !== source.priority || remark.trim() !== (source.policyRemark || '');
const changed = (policyEditable && policyChanged) || providerChanged;
return <div className="v2-source-policy-cell">
<Checkbox checked={enabled} disabled={!editable || !policyEditable || save.isPending} onChange={(event) => setEnabled(Boolean(event.target.checked))} aria-label="启用"></Checkbox>
<Input aria-label={`${source.sourceLabel} 优先级`} type="number" min="1" max="1000" value={String(priority)} disabled={!editable || !policyEditable || save.isPending} onChange={(value) => setPriority(Number(value))} />
<Input className="v2-source-provider-input" aria-label={`${source.sourceLabel} 提供方`} value={providerName} maxLength={128} disabled={!editable || save.isPending} onChange={setProviderName} placeholder="提供方,如 G7s" />
<Input className="v2-source-provider-evidence-input" aria-label={`${source.sourceLabel} 提供方核验依据`} value={providerEvidence} maxLength={255} disabled={!editable || save.isPending || !providerChanged} onChange={setProviderEvidence} placeholder={providerChanged ? '权威终端清单、厂商确认记录等(必填)' : '修改提供方后填写核验依据'} />
<Input className="v2-source-policy-remark-input" aria-label={`${source.sourceLabel} 策略备注`} value={remark} maxLength={200} disabled={!editable || !policyEditable || save.isPending} onChange={setRemark} placeholder={policyEditable ? '启停或优先级调整原因(可选)' : '协议融合快照不可调整策略'} />
<Button theme="solid" size="small" disabled={!editable || !changed || save.isPending || !source.sourceRef || priority < 1 || priority > 1000 || (providerChanged && !providerEvidence.trim())} onClick={() => save.mutate()}>{save.isPending ? '保存中' : policyEditable ? '保存策略' : '保存提供方'}</Button>
{save.isError ? <em role="alert">{save.error instanceof Error ? save.error.message : '保存失败'}</em> : null}
</div>;
}
function SourceIdentity({ source }: { source: VehicleLocationSourceEvidence }) {
return <div className="v2-source-cell v2-source-identity-cell"><span><strong>{source.sourceLabel}</strong>{source.recommended ? <Tag color="green" type="light" size="small"></Tag> : null}</span><small>{source.terminalLabel || source.sourceKind || '未维护终端'}</small></div>;
}
function SourceProtocol({ source }: { source: VehicleLocationSourceEvidence }) {
return <div className="v2-source-cell"><Tag color="blue" type="light" size="small">{source.protocol}</Tag><small>{source.selectedWithinProtocol ? '协议内已选' : '协议内候选'}</small></div>;
}
function SourceHealth({ source }: { source: VehicleLocationSourceEvidence }) {
return <div className="v2-source-cell v2-source-health-cell"><span><i className={source.online ? 'is-online' : 'is-offline'} /><strong>{source.online ? '在线' : '离线'}</strong></span><small>{source.qualityStatus || '未知'}{source.qualityReason ? ` · ${source.qualityReason}` : ''}</small></div>;
}
function SourceTiming({ source }: { source: VehicleLocationSourceEvidence }) {
return <div className="v2-source-cell"><strong><PlatformTime className="v2-ops-time" value={source.firstSeenAt} sourceZone="utc" /></strong><small> <PlatformTime className="v2-ops-time" value={source.receivedAt || source.eventTime} sourceZone="utc" /></small></div>;
}
function SourceInterval({ source }: { source: VehicleLocationSourceEvidence }) {
return <div className="v2-source-cell"><strong>{source.reportIntervalSec == null ? '—' : `${source.reportIntervalSec}s`}</strong><small>{source.reportSampleCount ? `${source.reportSampleCount.toLocaleString('zh-CN')} 个里程样本` : '暂无累计样本'}</small></div>;
}
function SourcePosition({ source }: { source: VehicleLocationSourceEvidence }) {
return <div className="v2-source-cell"><strong>{source.longitude == null || source.latitude == null ? '—' : `${source.longitude.toFixed(6)}, ${source.latitude.toFixed(6)}`}</strong><small>{number(source.speedKmh)} km/h · {number(source.totalMileageKm)} km</small></div>;
}
function SourceDecision({ source }: { source: VehicleLocationSourceEvidence }) {
return <div className="v2-source-cell v2-source-reason"><strong>{source.recommended ? '当前推荐' : source.selectedWithinProtocol ? '协议首选' : '备用来源'}</strong><small>{source.selectionReason || '等待选举说明'}</small></div>;
}
function sourceRowKey(source?: VehicleLocationSourceEvidence) {
return source?.sourceRef || `${source?.protocol ?? ''}-${source?.sourceLabel ?? ''}-${source?.terminalLabel ?? ''}`;
}
function SourcePolicySummary({ source, onEdit }: {
source: VehicleLocationSourceEvidence;
onEdit: () => void;
}) {
return <div className="v2-source-policy-summary">
<span><Tag color={source.enabled ? 'green' : 'grey'} type="light" size="small">{source.enabled ? '已启用' : '已禁用'}</Tag><b> {source.priority}</b></span>
<small>{source.providerOverride || '提供方未维护'}</small>
<Button theme="light" size="small" onClick={onEdit}>{source.sourceKind === 'CANONICAL' ? '维护提供方' : '维护策略'}</Button>
</div>;
}
function SourcePolicySheet({ vin, source, diagnostic, editable, onClose, onSaved }: {
vin: string;
source?: VehicleLocationSourceEvidence;
diagnostic: VehicleSourceDiagnostic;
editable: boolean;
onClose: () => void;
onSaved: (next: VehicleSourceDiagnostic) => void;
}) {
const mobileLayout = useMobileLayout();
return <WorkspaceSideSheet
className="v2-source-policy-sidesheet"
visible={Boolean(source)}
ariaLabel="车辆来源策略"
closeLabel="关闭车辆来源策略"
placement={mobileLayout ? 'bottom' : 'right'}
width={mobileLayout ? undefined : 460}
height={mobileLayout ? 'min(76dvh, 660px)' : undefined}
title={source?.sourceLabel ?? '来源策略'}
description={source ? `${source.terminalLabel || source.protocol} · ${source.sourceKind === 'CANONICAL' ? '仅维护提供方' : '来源策略'}` : '来源策略'}
badge={source ? source.enabled ? '策略已启用' : '策略已停用' : undefined}
badgeColor={source?.enabled ? 'green' : 'grey'}
summaryItems={source ? [
{ label: '实时状态', value: source.online ? '在线' : '离线', detail: source.qualityReason || source.qualityStatus || '等待质量判定', tone: source.online ? 'success' : 'warning' },
{ label: '当前优先级', value: source.priority, detail: source.enabled ? '参与来源选举' : '不参与来源选举', tone: source.enabled ? 'primary' : 'neutral' },
{ label: '选举结论', value: source.recommended ? '当前推荐' : source.selectedWithinProtocol ? '协议首选' : '备用来源', detail: source.selectionReason || '等待选举说明' }
] : []}
onCancel={onClose}
footerNote="策略修改需点击表单内的保存按钮后才会生效。"
primaryAction={{ label: '完成', onClick: onClose }}
>
{source ? <div className="v2-source-policy-sheet-content"><SourcePolicyEditor vin={vin} source={source} diagnostic={diagnostic} editable={editable && Boolean(source.sourceRef)} onSaved={(next) => { onSaved(next); onClose(); }} /></div> : null}
</WorkspaceSideSheet>;
}
function SourceDiagnosticTable({ vin, sources, diagnostic, editable, onSaved }: {
vin: string;
sources: VehicleLocationSourceEvidence[];
diagnostic: VehicleSourceDiagnostic;
editable: boolean;
onSaved: (next: VehicleSourceDiagnostic) => void;
}) {
const [editingSource, setEditingSource] = useState<VehicleLocationSourceEvidence>();
const columns = [
{ title: '来源 / 终端', dataIndex: 'sourceLabel', width: 180, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourceIdentity source={source} /> },
{ title: '协议', dataIndex: 'protocol', width: 120, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourceProtocol source={source} /> },
{ title: '在线 / 质量', dataIndex: 'online', width: 190, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourceHealth source={source} /> },
{ title: '首次 / 最近上报', dataIndex: 'firstSeenAt', width: 220, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourceTiming source={source} /> },
{ title: '上报周期', dataIndex: 'reportIntervalSec', width: 145, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourceInterval source={source} /> },
{ title: '位置 / 里程', dataIndex: 'longitude', width: 230, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourcePosition source={source} /> },
{ title: '选举结论', dataIndex: 'recommended', width: 220, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourceDecision source={source} /> },
{ title: '运维策略', dataIndex: 'policy', width: 200, render: (_: unknown, source: VehicleLocationSourceEvidence) => <SourcePolicySummary source={source} onEdit={() => setEditingSource(source)} /> }
];
return <><div className="v2-source-table-wrap"><Table
className="v2-source-table"
columns={columns}
dataSource={sources}
rowKey={sourceRowKey}
pagination={false}
scroll={{ x: 1505 }}
empty={null}
onRow={(source) => source ? ({ className: source.recommended ? 'is-recommended' : '' }) : ({})}
/></div>
<SourcePolicySheet vin={vin} source={editingSource} diagnostic={diagnostic} editable={editable} onClose={() => setEditingSource(undefined)} onSaved={onSaved} />
</>;
}
function SourceDiagnosticCards({ vin, sources, diagnostic, editable, onSaved }: {
vin: string;
sources: VehicleLocationSourceEvidence[];
diagnostic: VehicleSourceDiagnostic;
editable: boolean;
onSaved: (next: VehicleSourceDiagnostic) => void;
}) {
const [editingSource, setEditingSource] = useState<VehicleLocationSourceEvidence>();
return <><div className="v2-source-mobile-list">{sources.map((source) => <Card className={`v2-source-mobile-card${source.recommended ? ' is-recommended' : ''}`} bodyStyle={{ padding: 0 }} key={sourceRowKey(source)}>
<header><SourceIdentity source={source} /><SourceHealth source={source} /></header>
<Descriptions className="v2-source-mobile-descriptions" align="left" size="small" data={[
{ key: '协议状态', value: <SourceProtocol source={source} /> },
{ key: '首次 / 最近上报', value: <SourceTiming source={source} /> },
{ key: '上报周期', value: <SourceInterval source={source} /> },
{ key: '位置 / 里程', value: <SourcePosition source={source} /> },
{ key: '选举结论', value: <SourceDecision source={source} /> }
]} />
<section><strong></strong><SourcePolicySummary source={source} onEdit={() => setEditingSource(source)} /></section>
</Card>)}</div>
<SourcePolicySheet vin={vin} source={editingSource} diagnostic={diagnostic} editable={editable} onClose={() => setEditingSource(undefined)} onSaved={onSaved} />
</>;
}
function SourceDiagnosticWorkspace() {
const queryClient = useQueryClient();
const mobileLayout = useMobileLayout();
const [keyword, setKeyword] = useState('');
const deferredKeyword = useDeferredValue(keyword.trim());
const [selected, setSelected] = useState<VehicleCoverageRow>();
const [candidateOffset, setCandidateOffset] = useState(0);
const [filtersCollapsed, setFiltersCollapsed] = useState(() => mobileLayout);
useEffect(() => setCandidateOffset(0), [deferredKeyword]);
const candidates = useQuery({
queryKey: ['ops-source-candidates', deferredKeyword, candidateOffset],
queryFn: ({ signal }) => api.vehicleCoverage(new URLSearchParams({ keyword: deferredKeyword, bindingStatus: 'bound', limit: '20', offset: String(candidateOffset) }), signal),
enabled: deferredKeyword.length > 0,
staleTime: 15_000,
gcTime: QUERY_MEMORY.optionGcTime
});
const session = useQuery({ queryKey: ['ops-session'], queryFn: ({ signal }) => api.session(signal), staleTime: 60_000 });
const diagnostic = useQuery({
queryKey: ['ops-source-diagnostic', selected?.vin],
queryFn: ({ signal }) => api.vehicleSourceDiagnostic(selected!.vin, signal),
enabled: Boolean(selected?.vin),
staleTime: 5_000,
gcTime: QUERY_MEMORY.highVolumeGcTime
});
const choose = (vehicle: VehicleCoverageRow) => {
setSelected(vehicle);
setKeyword(vehicle.plate || vehicle.vin);
if (mobileLayout) setFiltersCollapsed(true);
};
const chooseFirst = () => {
const first = candidates.data?.items[0];
if (first) choose(first);
};
const submit = (event: FormEvent) => {
event.preventDefault();
chooseFirst();
};
const clearSelection = () => {
setKeyword('');
setSelected(undefined);
setFiltersCollapsed(true);
};
const data = diagnostic.data;
const editable = session.data?.role === 'admin';
const selectedLabel = selected ? selected.plate || selected.vin : '';
const browsingCandidates = Boolean(deferredKeyword) && deferredKeyword !== selectedLabel;
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
const closeMobileFilters = () => {
if (selectedLabel) setKeyword(selectedLabel);
setFiltersCollapsed(true);
};
useSideSheetA11y(mobileFiltersOpen, '.v2-source-mobile-filter-sidesheet', 'v2-source-mobile-filter-sheet', '诊断车辆筛选', '关闭诊断车辆筛选');
const filterStatus = selectedLabel
? `已选 ${selectedLabel}`
: deferredKeyword
? candidates.isFetching ? '正在查找' : `${candidates.data?.total ?? 0} 辆候选`
: '等待选择车辆';
const onSourceSaved = (next: VehicleSourceDiagnostic) => {
queryClient.setQueryData(['ops-source-diagnostic', next.evidence.vin], next);
void Promise.all([
queryClient.invalidateQueries({ queryKey: ['access-summary'] }),
queryClient.invalidateQueries({ queryKey: ['access-vehicles'] }),
queryClient.invalidateQueries({ queryKey: ['ops-source-readiness-v2'] })
]);
};
const sourceSearchInput = <Input
aria-label="按车牌或 VIN 搜索诊断车辆"
prefix={<IconSearch />}
value={keyword}
onChange={setKeyword}
placeholder="输入车牌或 VIN支持模糊搜索"
/>;
const sourceCandidateList = browsingCandidates ? <VehicleCandidateList
className="v2-source-candidates"
items={candidates.data?.items ?? []}
loading={candidates.isFetching}
loadingText="正在查询车辆…"
emptyText="没有匹配的已绑定车辆"
actionLabel="诊断"
showProtocols
onSelect={choose}
footer={(candidates.data?.total ?? 0) > 20 ? <><span> {Math.floor(candidateOffset / 20) + 1} / {Math.ceil((candidates.data?.total ?? 0) / 20)} </span><div><Button theme="light" size="small" disabled={candidateOffset === 0} onClick={() => setCandidateOffset(Math.max(0, candidateOffset - 20))}></Button><Button theme="light" size="small" disabled={candidateOffset + 20 >= (candidates.data?.total ?? 0)} onClick={() => setCandidateOffset(candidateOffset + 20)}></Button></div></> : undefined}
/> : null;
return <>
{mobileLayout ? <div className="v2-source-mobile-discovery">
<MobileFilterToggle
title="诊断车辆"
summary={selectedLabel ? `已选 ${selectedLabel}` : '尚未选择车辆'}
expanded={mobileFiltersOpen}
expandedLabel="关闭"
collapsedLabel="修改"
onToggle={() => mobileFiltersOpen ? closeMobileFilters() : setFiltersCollapsed(false)}
/>
<SideSheet
className="v2-source-mobile-filter-sidesheet"
visible={mobileFiltersOpen}
placement="bottom"
height="min(78dvh, 660px)"
aria-label="诊断车辆筛选"
title={<div className="v2-source-mobile-filter-title"><strong></strong><span> VIN </span></div>}
onCancel={closeMobileFilters}
footer={<div className="v2-source-mobile-filter-footer"><Button theme="light" disabled={!selected && !keyword} onClick={clearSelection}></Button><Button theme="solid" disabled={!browsingCandidates || !candidates.data?.items.length} onClick={chooseFirst}></Button></div>}
>
<form className="v2-source-mobile-filter-form" onSubmit={submit}>
<section className="v2-source-mobile-filter-section" aria-label="诊断车辆搜索">
<header><strong></strong><span>{selectedLabel ? `当前诊断 ${selectedLabel},选择其他车辆后再切换` : '每次只读取一辆车的来源证据,不扫描整车队'}</span></header>
{sourceSearchInput}
</section>
{sourceCandidateList}
</form>
</SideSheet>
</div> : <WorkspaceFilterPanel
className="v2-source-filter-panel"
title="诊断车辆"
description="按车牌或 VIN 选择一辆车,仅按需加载该车来源证据"
mobileSummary={selectedLabel ? `已选 ${selectedLabel}` : deferredKeyword ? `正在查找 ${deferredKeyword}` : '尚未选择车辆'}
expanded={!filtersCollapsed}
status={filterStatus}
statusColor={selectedLabel ? 'blue' : 'grey'}
collapsedLabel="修改"
onToggle={() => setFiltersCollapsed((value) => !value)}
>
<form className={`v2-source-search${filtersCollapsed ? ' is-mobile-collapsed' : ''}`} onSubmit={submit}>
{sourceSearchInput}
<Button htmlType="submit" theme="solid" disabled={!browsingCandidates || !candidates.data?.items.length}></Button>
{sourceCandidateList}
</form>
</WorkspaceFilterPanel>}
<Card className="v2-source-diagnostic" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
title="单车多来源诊断"
description={selected ? `${selected.plate || '未绑定车牌'} · ${selected.vin}` : '协议、同协议多终端、推荐原因与可审计策略'}
meta={<Tag color={data ? 'blue' : 'grey'} type="light" size="small">{data ? `${data.evidence.locationSources.length} 个来源` : selected ? '正在读取证据' : '等待选择车辆'}</Tag>}
actions={selected ? <Button theme="light" icon={<IconRefresh />} onClick={() => diagnostic.refetch()} disabled={diagnostic.isFetching}></Button> : null}
/>
{diagnostic.isError ? <InlineError message={diagnostic.error.message} onRetry={() => diagnostic.refetch()} /> : null}
{!selected ? <Empty className="v2-source-empty" title="先选择一辆车" description="将展示所有协议和同协议多终端、推荐原因、上报周期与可审计策略。" /> : null}
{selected && diagnostic.isPending ? <div className="v2-source-loading" role="status"><Spin size="large" /><strong></strong><span></span></div> : null}
{data ? <>
<section className="v2-source-summary-region" role="group" aria-label="车辆来源诊断概览">
<CardGroup className="v2-source-summary" type="grid" spacing={10}>
<Card className="v2-source-summary-card is-vehicle"><small></small><strong>{data.evidence.plate || selected?.plate || '未绑定车牌'}</strong><span>{data.evidence.vin}</span></Card>
<Card className="v2-source-summary-card is-recommended"><small></small><strong>{data.evidence.recommendedLocationLabel || '暂无推荐'}</strong><Tag color="blue" type="light" size="small">{data.evidence.recommendedLocationProtocol || '—'}</Tag></Card>
<Card className={`v2-source-summary-card is-source-health${data.evidence.locationConflict ? ' is-warning' : ' is-ok'}`}><small></small><strong>{data.evidence.locationSources.filter((item) => item.online).length} / {data.evidence.locationSources.length} 线</strong><span>{data.evidence.locationConflict ? `位置冲突 ${number(data.evidence.conflictDistanceM)}m` : '未发现实时位置冲突'}</span></Card>
<Card className="v2-source-summary-card is-policy"><small></small><strong>v{data.policy.version}</strong><span>{data.policy.updatedAt ? <>{data.policy.updatedBy} · <PlatformTime className="v2-ops-time" value={data.policy.updatedAt} sourceZone="utc" /></> : '尚无人工调整'}</span></Card>
</CardGroup>
</section>
<Card className="v2-source-recommendation"><header><Tag color="blue" type="light" size="small"></Tag><span>{data.refreshHint}</span></header><p>{data.recommendationReason}</p></Card>
{!editable ? <div className="v2-source-readonly"><Tag color="orange" type="light" size="small"></Tag><span></span></div> : null}
{mobileLayout
? <SourceDiagnosticCards vin={data.evidence.vin} sources={data.evidence.locationSources} diagnostic={data} editable={editable} onSaved={onSourceSaved} />
: <SourceDiagnosticTable vin={data.evidence.vin} sources={data.evidence.locationSources} diagnostic={data} editable={editable} onSaved={onSourceSaved} />}
<Card className="v2-source-audit" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="最近策略审计" description="仅记录实际变更,原始 source_key 不对前端暴露" />
{data.policy.audit.length ? <ol>{data.policy.audit.map((item) => <li key={`${item.changeType}-${item.version}-${item.sourceRef}`}><b>v{item.version}</b><span>{item.summary}</span><em>{item.changeType === 'provider' ? '提供方' : '策略'} · {item.actor} · <PlatformTime className="v2-ops-time" value={item.changedAt} sourceZone="utc" /></em></li>)}</ol> : <p></p>}
</Card>
</> : null}
</Card>
</>;
}
export default function OperationsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const requestedWorkspace = searchParams.get('view') as OperationsWorkspace | null;
const workspace = requestedWorkspace && operationsWorkspaces.has(requestedWorkspace) ? requestedWorkspace : 'reconciliation';
const setWorkspace = (next: OperationsWorkspace) => {
const copy = new URLSearchParams(searchParams);
if (next === 'reconciliation') copy.delete('view');
else copy.set('view', next);
setSearchParams(copy, { replace: true });
};
const health = useQuery({ queryKey: ['ops-health-v2'], queryFn: ({ signal }) => api.opsHealth(signal), refetchInterval: 15_000, staleTime: 8_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY });
const readiness = useQuery({ queryKey: ['ops-source-readiness-v2'], queryFn: ({ signal }) => api.sourceReadiness(signal), refetchInterval: 30_000, staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY });
const reconciliation = useQuery({ queryKey: ['reconciliation-summary', 30], queryFn: ({ signal }) => api.reconciliationSummary(30, signal), staleTime: 60_000, gcTime: QUERY_MEMORY.summaryGcTime });
const data = health.data; const sources = readiness.data;
const refresh = () => Promise.all([health.refetch(), readiness.refetch(), reconciliation.refetch()]);
const linkIssueCount = data?.linkHealth.filter((item) => item.status !== 'ok').length ?? 0;
const runtimeIssueCount = data ? Number(!data.mysqlWritable) + Number(!data.tdengineWritable) + Number(data.runtime.dataMode !== 'production') : 0;
const capacityIssueCount = data?.capacityFindings.length ?? 0;
const infrastructureIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0);
const sourceIssueCount = sources?.sources.filter((item) => item.severity !== 'ok').length ?? 0;
const sourceErrorCount = sources?.sources.filter((item) => item.severity === 'error').length ?? 0;
const sourceHealthyCount = sources?.sources.filter((item) => item.severity === 'ok').length ?? 0;
const readinessUnavailable = readiness.isError;
const healthIssueCount = infrastructureIssueCount + sourceIssueCount + Number(readinessUnavailable);
const healthEvidencePending = !data || readiness.isPending;
const overallStatus = healthEvidencePending
? 'unknown'
: readinessUnavailable || sourceErrorCount > 0
? 'error'
: healthIssueCount > 0
? 'warning'
: 'ok';
const overallValue = healthEvidencePending
? '读取中'
: readinessUnavailable
? '证据缺失'
: healthIssueCount > 0
? '需要处理'
: '运行正常';
const overallDetail = healthEvidencePending
? '等待运行与协议覆盖证据'
: readinessUnavailable
? '协议来源就绪度暂不可用'
: healthIssueCount > 0
? `协议覆盖 ${sourceIssueCount} 项 · 链路运行 ${infrastructureIssueCount}`
: '链路、写入与协议覆盖均正常';
const sortedLinks = useMemo(() => [...(data?.linkHealth ?? [])].sort((left, right) => {
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
return (priority[left.status] ?? 3) - (priority[right.status] ?? 3);
}), [data?.linkHealth]);
const sortedSources = useMemo(() => [...(sources?.sources ?? [])].sort((left, right) => {
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
return (priority[left.severity] ?? 3) - (priority[right.severity] ?? 3);
}), [sources?.sources]);
const workspaceContext = workspace === 'reconciliation'
? {
description: '每日自动对账',
status: reconciliation.data ? `${reconciliation.data.pending.toLocaleString('zh-CN')} 项待复核` : '读取差异证据',
color: reconciliation.data?.overSla ? 'orange' as const : 'blue' as const
}
: workspace === 'diagnostic'
? { description: '按需读取单车来源', status: '精确诊断', color: 'cyan' as const }
: {
description: '15 秒刷新运行证据',
status: healthEvidencePending ? '读取中' : readinessUnavailable ? '协议证据缺失' : healthIssueCount > 0 ? `${healthIssueCount} 项关注` : '运行正常',
color: healthEvidencePending ? 'grey' as const : readinessUnavailable || sourceErrorCount > 0 ? 'red' as const : healthIssueCount > 0 ? 'orange' as const : 'green' as const
};
return <div className={`v2-ops-page is-${workspace}`}>
<WorkspaceCommandBar
className="v2-ops-command-bar"
ariaLabel="运维质量操作"
title="运行与数据质量"
description="集中处理自动对账问题,按需诊断单车来源,并核对服务与协议健康;所有结论均可追溯。"
status={data?.runtime.dataMode === 'production' ? '生产模式' : '状态待确认'}
statusColor={data?.runtime.dataMode === 'production' ? 'green' : 'orange'}
meta={<Typography.Text type="tertiary"> · {data?.runtime.platformRelease ? `版本 ${data.runtime.platformRelease}` : '正在读取运行版本'}</Typography.Text>}
actions={<Button className="v2-workspace-mobile-tool-button is-muted" theme="light" icon={<IconRefresh />} aria-label="刷新全局证据" loading={health.isFetching || readiness.isFetching} onClick={refresh}></Button>}
/>
<nav className="v2-ops-navigation" aria-label="运维质量视图">
<SegmentedTabs
className="v2-ops-tabs"
variant="filled"
ariaLabel="运维质量工作区"
value={workspace}
onChange={setWorkspace}
items={[
{ key: 'reconciliation', label: '差异处置', count: reconciliation.data?.pending || undefined },
{ key: 'diagnostic', label: '单车诊断' },
{ key: 'health', label: '全局健康', count: healthIssueCount || undefined }
]}
/>
<div className="v2-ops-navigation-meta" aria-live="polite">
<Typography.Text type="tertiary">{workspaceContext.description}</Typography.Text>
<Tag color={workspaceContext.color} type="light" size="small">{workspaceContext.status}</Tag>
</div>
</nav>
<section className={`v2-ops-workspace is-${workspace}`} role="tabpanel" tabIndex={0} aria-label={workspace === 'reconciliation' ? '差异处置' : workspace === 'diagnostic' ? '单车诊断' : '全局健康'}>
{workspace === 'reconciliation' ? <ReconciliationCenter /> : null}
{workspace === 'diagnostic' ? <SourceDiagnosticWorkspace /> : null}
{workspace === 'health' ? <>
{health.isError ? <InlineError message={health.error.message} onRetry={refresh} /> : null}
{readiness.isError ? <InlineError message={readiness.error instanceof Error ? readiness.error.message : '协议来源就绪度读取失败'} onRetry={() => readiness.refetch()} /> : null}
<Card className="v2-ops-overview" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
title="运行概览"
description="关键链路、基础设施与车辆覆盖的同一健康快照"
meta={<span className="v2-ops-overview-meta"><HealthTag status={overallStatus}>{!data ? '正在读取' : healthIssueCount > 0 ? `${healthIssueCount} 项需关注` : '全部正常'}</HealthTag><Typography.Text type="tertiary">15 </Typography.Text></span>}
/>
<section className="v2-ops-metric-rail" role="list" aria-label="运行健康指标">
<OpsMetric label="整体状态" value={overallValue} detail={overallDetail} tone={healthEvidencePending ? 'neutral' : readinessUnavailable || sourceErrorCount > 0 ? 'danger' : healthIssueCount > 0 ? 'warning' : 'success'} />
<OpsMetric
label="协议就绪 / 总数"
value={sources?.sources.length ? `${sourceHealthyCount} / ${sources.sources.length}` : readinessUnavailable ? '不可用' : '—'}
detail={readinessUnavailable ? '来源证据读取失败' : sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需要处置` : sources?.sources.length ? '全部协议达到当前口径' : '等待协议覆盖证据'}
tone={readinessUnavailable || sourceErrorCount > 0 ? 'danger' : sourceIssueCount > 0 ? 'warning' : sources?.sources.length ? 'success' : 'neutral'}
/>
<OpsMetric label="车辆总数 / 在线" value={sources ? `${sources.totalVehicles} / ${sources.onlineVehicles}` : '—'} detail={sources ? `已绑定 ${sources.boundVehicles} · 待绑定 ${sources.identityRequiredVehicles}` : '档案与快照并集'} />
<OpsMetric label="活跃连接" value={data?.activeConnections?.toLocaleString('zh-CN') ?? '—'} detail="网关实时连接" />
<OpsMetric label="Kafka Lag" value={data?.kafkaLag?.toLocaleString('zh-CN') ?? '—'} detail={data?.kafkaLag === 0 ? '消费积压已回零' : data ? '存在待消费消息' : '等待消费证据'} tone={data?.kafkaLag === 0 ? 'success' : data ? 'warning' : 'neutral'} />
</section>
</Card>
<Card className={`v2-ops-panel v2-ops-sources${sourceIssueCount || readinessUnavailable ? ' has-attention' : ''}`} bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="优先展示覆盖不足的协议,并给出处置动作与验收口径" meta={<HealthTag status={readinessUnavailable ? 'error' : sourceIssueCount > 0 ? 'warning' : sources ? 'ok' : 'unknown'}>{readinessUnavailable ? '证据不可用' : sources ? sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需关注` : `${sources.sources.length} 个协议正常` : '正在读取'}</HealthTag>} />{sources ? sortedSources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sortedSources.map((source) => {
const onlineRate = sourceOnlineRate(source.online, source.total, source.onlineRate);
return <Card className={`v2-ops-source-card is-${source.severity}`} key={source.protocol} title={<span className="v2-ops-source-title"><Tag color="blue" type="light" size="small">{source.protocol}</Tag><small>{source.role}</small></span>} headerExtraContent={<HealthTag status={source.severity}>{source.status || statusLabel(source.severity)}</HealthTag>} headerLine>
<div className="v2-ops-source-coverage"><span><strong>线</strong><b>{source.online.toLocaleString('zh-CN')} / {source.total.toLocaleString('zh-CN')}</b></span><Progress aria-label={`${source.protocol} 在线覆盖率`} percent={onlineRate} showInfo={false} stroke="var(--v2-blue)" /><small>{number(onlineRate)}% 线{source.missingVehicles ? ` · ${source.missingVehicles.toLocaleString('zh-CN')} 辆待恢复` : ''}</small></div>
<div className="v2-ops-source-evidence"><strong></strong><p>{source.evidence}</p></div><div className="v2-ops-source-action"><strong></strong><p>{source.action}</p></div><footer><Tag color="green" type="light" size="small"></Tag><span>{source.acceptance}</span></footer>
</Card>;
})}</CardGroup> : <Empty className="v2-ops-source-empty" title="暂无协议来源" description="当前响应没有可展示的协议来源就绪度。" /> : readinessUnavailable ? <Empty className="v2-ops-source-empty is-error" title="协议来源证据不可用" description="运行证据仍可查看;请使用上方重试恢复协议覆盖判断。" /> : <div className="v2-ops-source-loading" role="status"><Spin size="middle" tip="正在读取协议来源就绪度" /></div>}</Card>
<div className="v2-ops-grid"><Card className="v2-ops-panel v2-ops-links" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="数据链路" description="按链路查看当前状态与服务端诊断证据" meta={<HealthTag status={!data ? 'unknown' : linkIssueCount > 0 ? 'warning' : 'ok'}>{!data ? '读取中' : linkIssueCount > 0 ? `${linkIssueCount} 条异常` : '链路正常'}</HealthTag>} /><div className="v2-ops-link-list" role="list">{sortedLinks.length ? sortedLinks.map((item) => {
const label = opsLinkLabel(item.name);
return <article className={`v2-ops-link-card is-${item.status}`} key={item.name} role="listitem"><span className="v2-ops-link-status"><i /><span><strong>{label}</strong>{label !== item.name ? <small>{item.name}</small> : null}</span></span><p>{item.detail || '无补充信息'}</p><HealthTag status={item.status} /></article>;
}) : <Empty className="v2-ops-link-empty" title={data ? '暂无链路探针' : '正在读取链路状态'} description={data ? '当前响应没有可展示的数据链路证据。' : '全局健康数据返回后将在这里更新。'} />}</div></Card>
<Card className="v2-ops-panel v2-ops-runtime" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="运行时安全" description="生产模式、写入探针与代理配置" meta={<HealthTag status={!data ? 'unknown' : runtimeIssueCount + capacityIssueCount > 0 ? 'warning' : 'ok'}>{!data ? '读取中' : runtimeIssueCount + capacityIssueCount > 0 ? `${runtimeIssueCount + capacityIssueCount} 项关注` : '配置正常'}</HealthTag>} /><Descriptions className="v2-ops-runtime-descriptions" align="left" size="small" data={[
{ key: '生产数据模式', value: <HealthTag status={data?.runtime.dataMode === 'production' ? 'ok' : 'error'}>{data?.runtime.dataMode === 'production' ? '已启用' : '未启用'}</HealthTag> },
{ key: 'MySQL 写探针', value: <HealthTag status={data?.mysqlWritable ? 'ok' : 'error'}>{data?.mysqlWritable ? '正常' : '异常'}</HealthTag> },
{ key: 'TDengine 写探针', value: <HealthTag status={data?.tdengineWritable ? 'ok' : 'error'}>{data?.tdengineWritable ? '正常' : '异常'}</HealthTag> },
{ key: '请求超时', value: `${data?.runtime.requestTimeoutMs ?? '—'} ms` },
{ key: '高德安全代理', value: <HealthTag status={data?.runtime.amapSecurityProxyEnabled && !data?.runtime.amapSecurityCodeExposed ? 'ok' : 'warning'}>{data?.runtime.amapSecurityProxyEnabled ? '服务端代理' : '未启用'}</HealthTag> }
]} />{data?.capacityFindings?.length ? <div className="v2-ops-capacity-findings">{data.capacityFindings.map((item) => <Card className="v2-ops-capacity-finding" key={item}><HealthTag status="warning"></HealthTag><p>{item}</p></Card>)}</div> : <Empty className="v2-ops-capacity-empty" image={<IconTickCircle />} title="容量检查通过" description="当前没有需要处理的容量风险。" />}</Card></div>
</> : null}
</section>
</div>;
}