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>
This commit is contained in:
2026-06-22 10:18:20 +08:00
parent 083d12016a
commit 1429442dbd
49 changed files with 2758 additions and 2141 deletions
+113
View File
@@ -0,0 +1,113 @@
'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { Alert, Badge, Breadcrumb as AntdBreadcrumb, Button, Card, Col, Empty, Flex, Row, Space, Spin, Typography, message } from 'antd';
import { ArrowLeftOutlined, CameraOutlined, FullscreenOutlined, HomeOutlined, ReloadOutlined } from '@ant-design/icons';
import { Breadcrumb } from '@/components/Breadcrumb';
import { CameraFrame } from '@/components/CameraFrame';
import { BackendApi } from '@/services/backendApi';
import type { CameraInfo } from '@/types';
const { Text } = Typography;
export default function VideoPage() {
const [cameras, setCameras] = useState<CameraInfo[]>([]);
const [fullscreenCamera, setFullscreenCamera] = useState<CameraInfo | null>(null);
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [messageApi, contextHolder] = message.useMessage();
const loadCameras = async () => {
try {
setLoading(true);
setErrorMessage('');
const cameraList = await BackendApi.getCameras();
setCameras(cameraList);
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : '摄像头列表加载失败,请稍后重试');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadCameras();
}, []);
if (fullscreenCamera) {
return (
<div>
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Space align="center" size="middle">
<Button icon={<ArrowLeftOutlined />} onClick={() => setFullscreenCamera(null)}></Button>
<AntdBreadcrumb
items={[
{ title: <Link href="/"><HomeOutlined /> </Link> },
{ title: <a href="#" onClick={(event) => { event.preventDefault(); setFullscreenCamera(null); }}></a> },
{ title: fullscreenCamera.name },
]}
/>
</Space>
<Button type="primary" icon={<CameraOutlined />} onClick={() => messageApi.info('截图接口暂未提供')}></Button>
</Flex>
<Flex justify="center" align="center" style={{ height: 'calc(100vh - 180px)', marginBottom: 16 }}>
<div style={{ height: '100%', maxWidth: '100%', aspectRatio: '16 / 9', boxShadow: '0 8px 24px rgba(0,0,0,0.1)' }}>
<CameraFrame camera={fullscreenCamera} height="100%" />
</div>
</Flex>
</div>
);
}
return (
<div>
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 24 }}>
<Breadcrumb />
<Space>
<Button icon={<ReloadOutlined />} onClick={loadCameras}></Button>
<Button icon={<FullscreenOutlined />} onClick={() => messageApi.info('请选择一个在线画面进入全屏')}></Button>
<Button type="primary" icon={<CameraOutlined />} onClick={() => messageApi.info('全部截图接口暂未提供')}></Button>
</Space>
</Flex>
{errorMessage && (
<Alert type="error" message={errorMessage} showIcon style={{ marginBottom: 24 }} />
)}
{loading ? (
<Flex vertical align="center" justify="center" style={{ padding: 64 }}>
<Spin size="large" tip="正在加载摄像头..." />
</Flex>
) : cameras.length ? (
<Row gutter={[24, 24]}>
{cameras.map((camera) => (
<Col xs={24} lg={12} key={camera.id}>
<Card
hoverable={camera.status === 'online'}
style={{ overflow: 'hidden', borderRadius: 12, borderColor: camera.status === 'online' ? '#f0f0f0' : '#ffccc7' }}
styles={{ body: { padding: 0 } }}
onClick={() => camera.status === 'online' && setFullscreenCamera(camera)}
>
<CameraFrame camera={camera} height={300} />
<Flex justify="space-between" align="center" style={{ padding: '12px 20px', background: camera.status === 'online' ? '#ffffff' : '#fff1f0', borderTop: '1px solid #f0f0f0' }}>
<Space size="middle">
<Badge status={camera.status === 'online' ? 'processing' : 'error'} />
<Text strong style={{ fontSize: 15, color: camera.status === 'online' ? 'inherit' : '#cf1322' }}>{camera.name}</Text>
{camera.placeholder && <Text type="secondary" style={{ fontSize: 12 }}></Text>}
</Space>
{camera.status === 'online' && <Button type="link" icon={<FullscreenOutlined />} size="small"></Button>}
</Flex>
</Card>
</Col>
))}
</Row>
) : (
<Empty description="暂无摄像头数据" />
)}
</div>
);
}