Files
smart-inspection/public-frontend/src/components/TopHeader.tsx
T
FaulknerWu 1429442dbd Rename customs-tablet-frontend to public-frontend and add new features
- Rename customs-tablet-frontend/ to public-frontend/ for broader scope
- Add new pages: customs, inspection with camera integration
- Add new services: apiClient.ts, backendApi.ts, normalizers.ts
- Add CameraFrame component for real-time video streaming
- Add scan_fixer module with clock_publisher and timestamp fix utilities
- Update startup scripts to support new frontend structure
- Update arm_server configuration and service files

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:18:20 +08:00

145 lines
5.1 KiB
TypeScript

'use client';
import React, { useEffect, useState } from 'react';
import { Avatar, Badge, Button, Dropdown, Layout, Menu, Space, Switch, Typography, message, theme } from 'antd';
import {
BellOutlined,
DashboardOutlined,
FileTextOutlined,
SafetyCertificateOutlined,
ScanOutlined,
SearchOutlined,
SettingOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { usePathname, useRouter } from 'next/navigation';
import { BackendApi } from '@/services/backendApi';
import { useAppStore } from '@/store/useAppStore';
import type { ApiMode } from '@/types';
const { Header } = Layout;
const { Text, Paragraph, Title } = Typography;
export const TopHeader: React.FC = () => {
const pathname = usePathname();
const router = useRouter();
const { user, notifications } = useAppStore();
const { token } = theme.useToken();
const [apiMode, setApiMode] = useState<ApiMode | null>(null);
const [switchingMode, setSwitchingMode] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const unreadCount = notifications.filter((notification) => !notification.read).length;
useEffect(() => {
BackendApi.getApiMode()
.then(setApiMode)
.catch(() => setApiMode(null));
}, []);
const menuItems = [
{ key: '/', icon: <DashboardOutlined />, label: '首页' },
{ key: '/video', icon: <VideoCameraOutlined />, label: '视频监控' },
{ key: '/machines', icon: <SearchOutlined />, label: '机器查询' },
{ key: '/customs', icon: <FileTextOutlined />, label: '报关单' },
{ key: '/inspection', icon: <ScanOutlined />, label: '远程查验' },
];
const notificationMenu = {
items: notifications.map((notification) => ({
key: notification.id,
label: (
<div style={{ width: 250, padding: '4px 0', whiteSpace: 'normal' }}>
<Text strong={!notification.read} type={notification.read ? 'secondary' : undefined} style={{ display: 'block' }}>
{notification.title}
</Text>
<Paragraph style={{ marginTop: 4, marginBottom: 0, color: token.colorTextSecondary, fontSize: 12, lineHeight: 1.5 }}>
{notification.message}
</Paragraph>
<Text style={{ marginTop: 4, display: 'block', color: token.colorTextTertiary, fontSize: 10 }}>
{notification.time}
</Text>
</div>
),
})),
};
const toggleApiMode = async (nextTestMode: boolean) => {
setSwitchingMode(true);
try {
const nextMode = await BackendApi.setApiMode(nextTestMode);
setApiMode(nextMode);
messageApi.success(`已切换至${nextMode.label}`);
} catch (error) {
messageApi.error(error instanceof Error ? error.message : '切换 API 环境失败');
} finally {
setSwitchingMode(false);
}
};
return (
<>
{contextHolder}
<Header
style={{
position: 'sticky',
top: 0,
zIndex: 100,
display: 'flex',
alignItems: 'center',
padding: '0 24px',
background: token.colorBgContainer,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
}}
>
<Space
size="middle"
onClick={() => router.push('/')}
style={{ cursor: 'pointer', marginRight: 48, display: 'flex', alignItems: 'center' }}
>
<SafetyCertificateOutlined style={{ fontSize: 24, color: token.colorPrimary }} />
<Title level={4} style={{ margin: 0, color: token.colorPrimary, whiteSpace: 'nowrap' }}>
</Title>
</Space>
<Menu
mode="horizontal"
selectedKeys={[pathname === '/' ? '/' : `/${pathname.split('/')[1]}`]}
items={menuItems}
onClick={({ key }) => router.push(key)}
style={{ flex: 1, minWidth: 0, borderBottom: 'none', lineHeight: '62px' }}
/>
<Space size="large" style={{ marginLeft: 24, display: 'flex', alignItems: 'center' }}>
<Space size={8}>
<Text type="secondary" style={{ fontSize: 12 }}>{apiMode?.label ?? '环境'}</Text>
<Switch
size="small"
checked={apiMode?.testMode ?? false}
checkedChildren="测"
unCheckedChildren="正"
loading={switchingMode}
onChange={toggleApiMode}
/>
</Space>
<Dropdown menu={notificationMenu} placement="bottomRight" trigger={['click']}>
<Badge count={unreadCount} size="small" offset={[-4, 4]}>
<Button type="text" shape="circle" icon={<BellOutlined style={{ fontSize: 18, color: token.colorText }} />} />
</Badge>
</Dropdown>
<Button type="text" shape="circle" icon={<SettingOutlined style={{ fontSize: 18, color: token.colorText }} />} />
<Space size="small" style={{ cursor: 'pointer', padding: '0 8px', borderRadius: token.borderRadius }}>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: token.colorPrimary }} />
<Text style={{ fontSize: 14 }}>{user?.name}</Text>
</Space>
</Space>
</Header>
</>
);
};