178 lines
7.6 KiB
TypeScript
178 lines
7.6 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 navItems = [
|
||
{ itemKey: 'dashboard', text: '运营驾驶舱', icon: <IconHome /> },
|
||
{ itemKey: 'map', text: '实时地图', icon: <IconMapPin /> },
|
||
{ itemKey: 'vehicles', text: '车辆中心', icon: <IconServer /> },
|
||
{ itemKey: 'realtime', text: '实时数据', icon: <IconActivity /> },
|
||
{ itemKey: 'detail', text: '车辆档案', icon: <IconSetting /> },
|
||
{ itemKey: 'history', text: '轨迹回放', icon: <IconMapPin /> },
|
||
{ itemKey: 'history-query', text: '历史查询', icon: <IconHistogram /> },
|
||
{ itemKey: 'mileage', text: '统计查询', icon: <IconBarChartHStroked /> },
|
||
{ itemKey: 'alert-events', text: '告警事件', icon: <IconHistogram /> },
|
||
{ itemKey: 'notification-rules', text: '通知规则', icon: <IconBell /> },
|
||
{ 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));
|
||
};
|
||
|
||
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>
|
||
<Nav
|
||
selectedKeys={[selectedPage]}
|
||
items={navItems}
|
||
onSelect={({ itemKey }) => onChange(itemKey as PageKey)}
|
||
style={{ maxWidth: '100%' }}
|
||
/>
|
||
</aside>
|
||
<main className="vp-main">
|
||
<header className="vp-topbar">
|
||
<Space spacing={12}>
|
||
<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>
|
||
<Space spacing={12}>
|
||
{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="blue">生产环境</Tag>
|
||
{platformRelease ? <Tag color="blue">版本 {platformRelease}</Tag> : null}
|
||
<Tag color="grey">32960 / 808 / MQTT 归一</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图就绪' : '地图待配置'}</Tag>
|
||
<Button size="small" aria-label="顶部地图态势" onClick={() => onChange('map')}>实时地图</Button>
|
||
<Button size="small" aria-label="顶部实时监控" onClick={() => onChange('realtime')}>实时监控</Button>
|
||
<Button size="small" aria-label="顶部轨迹回放" onClick={() => onChange('history')}>轨迹回放</Button>
|
||
<Button size="small" aria-label="顶部历史查询" onClick={() => onChange('history-query')}>历史查询</Button>
|
||
<Button size="small" aria-label="顶部统计查询" onClick={() => onChange('mileage')}>统计查询</Button>
|
||
<Button size="small" aria-label="顶部通知规则" onClick={() => onChange('notification-rules')}>通知规则</Button>
|
||
<Button size="small" aria-label="顶部运维质量" onClick={() => onChange('ops-quality')}>运维质量</Button>
|
||
<Button
|
||
size="small"
|
||
aria-label={`顶部${alertLabel}`}
|
||
className={alertClassName}
|
||
onClick={() => onChange('alert-events')}
|
||
>
|
||
{alertLabel}
|
||
</Button>
|
||
</Space>
|
||
</header>
|
||
{children}
|
||
</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|