233 lines
8.9 KiB
TypeScript
233 lines
8.9 KiB
TypeScript
import { Button, Input, Nav, Space, Tag, Toast } from '@douyinfe/semi-ui';
|
||
import {
|
||
IconActivity,
|
||
IconBarChartHStroked,
|
||
IconHistogram,
|
||
IconHome,
|
||
IconMapPin,
|
||
IconSearch,
|
||
IconServer,
|
||
IconSetting,
|
||
IconBell,
|
||
IconPulse
|
||
} from '@douyinfe/semi-icons';
|
||
import type { ReactNode } from 'react';
|
||
import { useState } from 'react';
|
||
import type { VehicleSourceConsistency, VehicleServiceStatus } from '../api/types';
|
||
import { isAMapConfigured } from '../config/appConfig';
|
||
|
||
export type PageKey = 'dashboard' | 'vehicles' | 'map' | 'realtime' | 'detail' | 'history' | 'history-query' | 'mileage' | 'alert-events' | 'quality' | 'notification-rules' | 'ops-quality';
|
||
|
||
const navGroups = [
|
||
{
|
||
title: '车辆监控',
|
||
audience: '客户',
|
||
description: '实时看车在哪里、是否在线、是否异常',
|
||
items: [
|
||
{ itemKey: 'dashboard', text: '运营总览', icon: <IconHome /> },
|
||
{ itemKey: 'map', text: '实时地图', icon: <IconMapPin /> },
|
||
{ itemKey: 'realtime', text: '实时监控', icon: <IconActivity /> }
|
||
]
|
||
},
|
||
{
|
||
title: '车辆资产',
|
||
audience: '客户',
|
||
description: '按 VIN、车牌、手机号管理车辆资产',
|
||
items: [
|
||
{ itemKey: 'vehicles', text: '车辆中心', icon: <IconServer /> },
|
||
{ itemKey: 'detail', text: '车辆档案', icon: <IconSetting /> }
|
||
]
|
||
},
|
||
{
|
||
title: '轨迹与统计',
|
||
audience: '客户',
|
||
description: '按时间窗回放轨迹、核对里程、导出数据',
|
||
items: [
|
||
{ itemKey: 'history', text: '轨迹回放', icon: <IconMapPin /> },
|
||
{ itemKey: 'mileage', text: '里程统计', icon: <IconBarChartHStroked /> },
|
||
{ itemKey: 'history-query', text: '历史查询/导出', icon: <IconHistogram /> }
|
||
]
|
||
},
|
||
{
|
||
title: '告警通知',
|
||
audience: '客户',
|
||
description: '断链、离线、定位异常和通知升级',
|
||
items: [
|
||
{ itemKey: 'alert-events', text: '告警事件', icon: <IconHistogram /> },
|
||
{ itemKey: 'notification-rules', text: '通知规则', icon: <IconBell /> }
|
||
]
|
||
},
|
||
{
|
||
title: '内部运维',
|
||
audience: '内部',
|
||
description: '客户主流程外查看接入链路和运行质量',
|
||
items: [
|
||
{ itemKey: 'ops-quality', text: '运维质量', icon: <IconPulse /> }
|
||
]
|
||
}
|
||
];
|
||
|
||
function linkHealthClassName(count: number | null) {
|
||
if (count == null) {
|
||
return '';
|
||
}
|
||
return count > 0 ? 'vp-link-health-button-warning' : 'vp-link-health-button-ok';
|
||
}
|
||
|
||
function alertButtonLabel(linkIssueCount: number | null, activeRuleCount?: number | null, p0RuleCount?: number | null) {
|
||
const activeCount = Number.isFinite(Number(activeRuleCount)) ? Number(activeRuleCount) : 0;
|
||
const p0Count = Number.isFinite(Number(p0RuleCount)) ? Number(p0RuleCount) : 0;
|
||
if (p0Count > 0) {
|
||
return `告警事件 P0 ${p0Count.toLocaleString()} / 活跃 ${activeCount.toLocaleString()}`;
|
||
}
|
||
if (activeCount > 0) {
|
||
return `告警事件 ${activeCount.toLocaleString()}类规则`;
|
||
}
|
||
if (linkIssueCount == null) {
|
||
return '告警事件';
|
||
}
|
||
return linkIssueCount > 0 ? `告警事件 ${linkIssueCount}项关注` : '告警事件正常';
|
||
}
|
||
|
||
function alertButtonClassName(linkIssueCount: number | null, activeRuleCount?: number | null, p0RuleCount?: number | null) {
|
||
const activeCount = Number.isFinite(Number(activeRuleCount)) ? Number(activeRuleCount) : 0;
|
||
const p0Count = Number.isFinite(Number(p0RuleCount)) ? Number(p0RuleCount) : 0;
|
||
if (p0Count > 0 || activeCount > 0 || (linkIssueCount ?? 0) > 0) {
|
||
return 'vp-link-health-button-warning';
|
||
}
|
||
return linkHealthClassName(linkIssueCount);
|
||
}
|
||
|
||
export function AppShell({
|
||
activePage,
|
||
linkIssueCount,
|
||
activeAlertRuleCount,
|
||
p0AlertRuleCount,
|
||
platformRelease,
|
||
currentVehicleStatus,
|
||
currentVehicleLabel,
|
||
currentVehicleConsistency,
|
||
onChange,
|
||
onVehicleSearch,
|
||
children
|
||
}: {
|
||
activePage: PageKey;
|
||
linkIssueCount: number | null;
|
||
activeAlertRuleCount?: number | null;
|
||
p0AlertRuleCount?: number | null;
|
||
platformRelease?: string;
|
||
currentVehicleStatus?: VehicleServiceStatus;
|
||
currentVehicleLabel?: string;
|
||
currentVehicleConsistency?: VehicleSourceConsistency;
|
||
onChange: (page: PageKey) => void;
|
||
onVehicleSearch: (keyword: string) => void | Promise<void>;
|
||
children: ReactNode;
|
||
}) {
|
||
const [keyword, setKeyword] = useState('');
|
||
const [searching, setSearching] = useState(false);
|
||
const amapConfigured = isAMapConfigured();
|
||
const alertLabel = alertButtonLabel(linkIssueCount, activeAlertRuleCount, p0AlertRuleCount);
|
||
const alertClassName = alertButtonClassName(linkIssueCount, activeAlertRuleCount, p0AlertRuleCount);
|
||
const selectedPage = activePage === 'quality' ? 'alert-events' : activePage;
|
||
|
||
const search = () => {
|
||
const value = keyword.trim();
|
||
if (!value) {
|
||
Toast.warning('请输入 VIN / 车牌 / 手机号');
|
||
return;
|
||
}
|
||
setSearching(true);
|
||
Promise.resolve(onVehicleSearch(value)).finally(() => setSearching(false));
|
||
};
|
||
|
||
const quickActions = [
|
||
{ label: '地图', ariaLabel: '顶部地图态势', page: 'map' as const },
|
||
{ label: '监控', ariaLabel: '顶部实时监控', page: 'realtime' as const },
|
||
{ label: '轨迹', ariaLabel: '顶部轨迹回放', page: 'history' as const },
|
||
{ label: '里程', ariaLabel: '顶部里程统计', page: 'mileage' as const },
|
||
{ label: '导出', ariaLabel: '顶部数据导出', page: 'history-query' as const },
|
||
{ label: '告警', ariaLabel: '顶部告警事件', page: 'alert-events' as const }
|
||
];
|
||
|
||
return (
|
||
<div className="vp-app">
|
||
<div className="vp-shell">
|
||
<aside className="vp-sidebar">
|
||
<div className="vp-brand">
|
||
<span className="vp-brand-mark" />
|
||
<span className="vp-brand-copy">
|
||
<span>车辆服务中台</span>
|
||
<span>地图监控 / 轨迹回放 / 告警通知</span>
|
||
</span>
|
||
</div>
|
||
<div className="vp-nav-groups">
|
||
{navGroups.map((group) => (
|
||
<div key={group.title} className="vp-nav-group">
|
||
<div className="vp-nav-section-title">
|
||
<span className="vp-nav-section-title-main">
|
||
<span>{group.title}</span>
|
||
<Tag size="small" color={group.audience === '内部' ? 'grey' : 'blue'}>{group.audience}</Tag>
|
||
</span>
|
||
<small>{group.description}</small>
|
||
</div>
|
||
<Nav
|
||
selectedKeys={[selectedPage]}
|
||
items={group.items}
|
||
onSelect={({ itemKey }) => onChange(itemKey as PageKey)}
|
||
style={{ maxWidth: '100%' }}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</aside>
|
||
<main className="vp-main">
|
||
<header className="vp-topbar">
|
||
<Space spacing={12} className="vp-topbar-search">
|
||
<Input
|
||
prefix={<IconSearch />}
|
||
placeholder="搜索 VIN / 车牌 / 手机号"
|
||
value={keyword}
|
||
onChange={setKeyword}
|
||
onEnterPress={search}
|
||
style={{ width: 320 }}
|
||
/>
|
||
<Button theme="solid" type="primary" loading={searching} onClick={search}>查询车辆</Button>
|
||
</Space>
|
||
<div className="vp-topbar-context">
|
||
{currentVehicleLabel ? <Tag color="grey">{currentVehicleLabel}</Tag> : null}
|
||
{currentVehicleStatus ? (
|
||
<Tag color={currentVehicleStatus.severity === 'ok' ? 'green' : currentVehicleStatus.severity === 'error' ? 'red' : 'orange'}>
|
||
当前车辆:{currentVehicleStatus.title}
|
||
</Tag>
|
||
) : null}
|
||
{currentVehicleConsistency ? (
|
||
<Tag color={currentVehicleConsistency.severity === 'ok' ? 'green' : currentVehicleConsistency.severity === 'error' ? 'red' : 'orange'}>
|
||
一致性:{currentVehicleConsistency.title || `${currentVehicleConsistency.onlineSourceCount}/${currentVehicleConsistency.sourceCount} 来源`}
|
||
</Tag>
|
||
) : null}
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图就绪' : '高德地图待配置'}</Tag>
|
||
<div className="vp-topbar-quick-actions" aria-label="车辆服务快捷入口">
|
||
{quickActions.map((item) => (
|
||
<Button key={item.page} size="small" aria-label={item.ariaLabel} onClick={() => onChange(item.page)}>
|
||
{item.label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
<Button
|
||
size="small"
|
||
aria-label={`顶部${alertLabel}`}
|
||
className={alertClassName}
|
||
onClick={() => onChange('alert-events')}
|
||
>
|
||
{alertLabel}
|
||
</Button>
|
||
{platformRelease ? <span className="vp-topbar-release">版本 {platformRelease}</span> : null}
|
||
</div>
|
||
</header>
|
||
{children}
|
||
</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|