Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/layout/AppShell.tsx

178 lines
7.6 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 { 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>
);
}