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

144 lines
6.2 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' | 'quality' | 'notification-rules' | 'ops-quality';
const navItems = [
{ itemKey: 'dashboard', text: '运营驾驶舱', icon: <IconHome /> },
{ itemKey: 'vehicles', text: '车辆中心', icon: <IconServer /> },
{ itemKey: 'map', text: '地图态势', icon: <IconMapPin /> },
{ 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: 'quality', 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';
}
export function AppShell({
activePage,
linkIssueCount,
platformRelease,
currentVehicleStatus,
currentVehicleLabel,
currentVehicleConsistency,
onChange,
onVehicleSearch,
children
}: {
activePage: PageKey;
linkIssueCount: 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 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></span>
</div>
<Nav
selectedKeys={[activePage]}
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"> / </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={linkIssueCount == null ? '顶部告警事件' : linkIssueCount > 0 ? `顶部告警事件 ${linkIssueCount}项关注` : '顶部告警事件正常'}
className={linkHealthClassName(linkIssueCount)}
onClick={() => onChange('quality')}
>
{linkIssueCount == null ? '告警事件' : linkIssueCount > 0 ? `告警事件 ${linkIssueCount}项关注` : '告警事件正常'}
</Button>
</Space>
</header>
{children}
</main>
</div>
</div>
);
}