125 lines
4.7 KiB
TypeScript
125 lines
4.7 KiB
TypeScript
import { Button, Input, Nav, Space, Tag, Toast } from '@douyinfe/semi-ui';
|
||
import {
|
||
IconActivity,
|
||
IconBarChartHStroked,
|
||
IconHistogram,
|
||
IconHome,
|
||
IconMapPin,
|
||
IconSearch,
|
||
IconServer,
|
||
IconSetting
|
||
} 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' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality';
|
||
|
||
const navItems = [
|
||
{ itemKey: 'dashboard', text: '运营总览', icon: <IconHome /> },
|
||
{ itemKey: 'vehicles', text: '车辆中心', icon: <IconServer /> },
|
||
{ itemKey: 'realtime', text: '实时监控', icon: <IconActivity /> },
|
||
{ itemKey: 'detail', text: '车辆档案', icon: <IconSetting /> },
|
||
{ itemKey: 'history', text: '轨迹回放', icon: <IconMapPin /> },
|
||
{ itemKey: 'mileage', text: '统计分析', icon: <IconBarChartHStroked /> },
|
||
{ itemKey: 'quality', text: '告警通知', icon: <IconHistogram /> }
|
||
];
|
||
|
||
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,
|
||
currentVehicleStatus,
|
||
currentVehicleLabel,
|
||
currentVehicleConsistency,
|
||
onChange,
|
||
onVehicleSearch,
|
||
children
|
||
}: {
|
||
activePage: PageKey;
|
||
linkIssueCount: number | null;
|
||
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>
|
||
<Tag color="grey">多源接入 / 一个车辆服务</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图就绪' : '地图待配置'}</Tag>
|
||
<Button size="small" onClick={() => onChange('realtime')}>实时地图</Button>
|
||
<Button size="small" onClick={() => onChange('history')}>地图回放</Button>
|
||
<Button size="small" className={linkHealthClassName(linkIssueCount)} onClick={() => onChange('quality')}>
|
||
{linkIssueCount == null ? '链路监控' : linkIssueCount > 0 ? `链路 ${linkIssueCount} 项关注` : '链路正常'}
|
||
</Button>
|
||
</Space>
|
||
</header>
|
||
{children}
|
||
</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|