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:
@@ -0,0 +1,243 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Button, Card, Col, DatePicker, Form, Input, Row, Select, Space, Table, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { PlayCircleOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import dayjs from 'dayjs';
|
||||
import { Breadcrumb } from '@/components/Breadcrumb';
|
||||
import { StatusBadge } from '@/components/StatusBadge';
|
||||
import { BackendApi } from '@/services/backendApi';
|
||||
import { useAppStore } from '@/store/useAppStore';
|
||||
import type { CustomsDeclaration, InspectionItem } from '@/types';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
export default function CustomsPage() {
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<CustomsDeclaration[]>([]);
|
||||
const [filteredData, setFilteredData] = useState<CustomsDeclaration[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [startingId, setStartingId] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [form] = Form.useForm();
|
||||
const { setSelectedCustoms, setInspection } = useAppStore();
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadCustomsList = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErrorMessage('');
|
||||
const customsList = await BackendApi.getCustomsList(1, 100);
|
||||
if (!isMounted) return;
|
||||
setData(customsList);
|
||||
setFilteredData(customsList);
|
||||
} catch (error) {
|
||||
if (!isMounted) return;
|
||||
setErrorMessage(error instanceof Error ? error.message : '报关单列表加载失败,请稍后重试');
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadCustomsList();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
const values = form.getFieldsValue();
|
||||
const keyword = values.searchText?.trim().toLowerCase() || '';
|
||||
const status = values.statusFilter || 'all';
|
||||
const dateRange = values.dateRange;
|
||||
|
||||
const nextData = data.filter((item) => {
|
||||
const matchesStatus = status === 'all' || item.status === status;
|
||||
const matchesKeyword = !keyword || item.customsName.toLowerCase().includes(keyword) || item.customsId.toLowerCase().includes(keyword);
|
||||
const createdAt = dayjs(item.createdAt);
|
||||
const matchesDateRange = !dateRange?.[0] || !dateRange?.[1] || !createdAt.isValid()
|
||||
|| (createdAt.isAfter(dateRange[0].startOf('day')) && createdAt.isBefore(dateRange[1].endOf('day')));
|
||||
|
||||
return matchesStatus && matchesKeyword && matchesDateRange;
|
||||
});
|
||||
|
||||
setFilteredData(nextData);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
setFilteredData(data);
|
||||
};
|
||||
|
||||
const loadExpandedItems = async (record: CustomsDeclaration): Promise<CustomsDeclaration> => {
|
||||
if (record.items.length) {
|
||||
return record;
|
||||
}
|
||||
const items = await BackendApi.getCustomsMachines(record.id);
|
||||
const nextRecord = {
|
||||
...record,
|
||||
items,
|
||||
machineCount: record.machineCount || items.reduce((sum, item) => sum + item.quantify, 0),
|
||||
};
|
||||
setData((current) => current.map((item) => (item.id === record.id ? nextRecord : item)));
|
||||
setFilteredData((current) => current.map((item) => (item.id === record.id ? nextRecord : item)));
|
||||
return nextRecord;
|
||||
};
|
||||
|
||||
const handleStartInspection = async (record: CustomsDeclaration) => {
|
||||
setStartingId(record.id);
|
||||
try {
|
||||
const nextRecord = await loadExpandedItems(record);
|
||||
const inspection = await BackendApi.startCustomsInspection(nextRecord);
|
||||
setSelectedCustoms(nextRecord);
|
||||
setInspection(inspection);
|
||||
messageApi.success('查验已开始');
|
||||
router.push(`/inspection?customsId=${encodeURIComponent(nextRecord.id)}`);
|
||||
} catch (error) {
|
||||
messageApi.error(error instanceof Error ? error.message : '开始查验失败');
|
||||
} finally {
|
||||
setStartingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const itemColumns: ColumnsType<InspectionItem> = useMemo(() => [
|
||||
{ title: '料号', dataIndex: 'inventoryCode', key: 'inventoryCode' },
|
||||
{ title: '品名', dataIndex: 'inventoryName', key: 'inventoryName' },
|
||||
{ title: '规格', dataIndex: 'spec', key: 'spec' },
|
||||
{ title: '总数', dataIndex: 'quantify', key: 'quantify' },
|
||||
{ title: '已查验', dataIndex: 'inspected', key: 'inspected' },
|
||||
], []);
|
||||
|
||||
const expandedRowRender = (record: CustomsDeclaration) => (
|
||||
<Table
|
||||
columns={itemColumns}
|
||||
dataSource={record.items}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowKey={(item) => item.inventoryCode}
|
||||
locale={{ emptyText: '展开后端机器列表为空,或暂未加载' }}
|
||||
/>
|
||||
);
|
||||
|
||||
const columns: ColumnsType<CustomsDeclaration> = [
|
||||
{
|
||||
title: '报关单号',
|
||||
dataIndex: 'customsName',
|
||||
key: 'customsName',
|
||||
render: (text: string) => <b>{text}</b>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: CustomsDeclaration['status']) => <StatusBadge status={status} />,
|
||||
},
|
||||
{
|
||||
title: '机器总数',
|
||||
dataIndex: 'machineCount',
|
||||
key: 'machineCount',
|
||||
render: (count: number) => (count ? `${count} 台` : '-'),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={startingId === record.id}
|
||||
onClick={() => handleStartInspection(record)}
|
||||
>
|
||||
开始查验
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Breadcrumb />
|
||||
|
||||
{errorMessage && (
|
||||
<Alert type="error" message={errorMessage} showIcon style={{ marginBottom: 16 }} />
|
||||
)}
|
||||
|
||||
<Card title="筛选条件" style={{ marginBottom: 24 }}>
|
||||
<Form form={form} layout="inline">
|
||||
<Row gutter={24} align="middle">
|
||||
<Col>
|
||||
<Form.Item label="状态" name="statusFilter" initialValue="all">
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'pending', label: '待查验' },
|
||||
{ value: 'inspecting', label: '查验中' },
|
||||
{ value: 'released', label: '已放行' },
|
||||
{ value: 'abnormal', label: '异常' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Item label="日期范围" name="dateRange">
|
||||
<RangePicker />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Item name="searchText">
|
||||
<Input
|
||||
placeholder="搜索报关单号..."
|
||||
prefix={<SearchOutlined />}
|
||||
onPressEnter={handleSearch}
|
||||
style={{ width: 250 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space>
|
||||
<Button type="primary" onClick={handleSearch}>查询</Button>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card title="报关单列表" styles={{ body: { padding: 0 } }}>
|
||||
<Table
|
||||
dataSource={filteredData}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
expandable={{
|
||||
expandedRowRender,
|
||||
onExpand: (expanded, record) => {
|
||||
if (expanded && !record.items.length) {
|
||||
loadExpandedItems(record).catch((error) => {
|
||||
messageApi.error(error instanceof Error ? error.message : '机器列表加载失败');
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user