Enhance inspection workspace

This commit is contained in:
2026-06-20 11:20:04 +08:00
parent f10ef75852
commit 083d12016a
4 changed files with 374 additions and 214 deletions
@@ -1,19 +1,19 @@
'use client'; 'use client';
import React, { Suspense, useEffect, useState, useRef } from 'react'; import React, { Suspense, useEffect, useState, useRef } from 'react';
import { Alert, Row, Col, Card, Button, Progress, List, Typography, Space, Modal, Input, Empty, Badge, Spin, Flex, Select, Segmented, theme, Divider, Timeline } from 'antd'; import { Row, Col, Card, Button, Progress, List, Typography, Space, Modal, Input, Empty, Badge, Spin, Flex, Select, theme, Timeline, Table, Tag } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { import {
PlayCircleOutlined, PlayCircleOutlined,
PauseCircleOutlined, PauseCircleOutlined,
StopOutlined, StopOutlined,
ReloadOutlined, ReloadOutlined,
CaretRightOutlined, CaretRightOutlined,
PauseCircleFilled, PauseCircleFilled
VideoCameraOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Breadcrumb } from '../../components/Breadcrumb'; import { Breadcrumb } from '../../components/Breadcrumb';
import { MockApi } from '../../services/mockApi'; import { MockApi } from '../../services/mockApi';
import { CustomsDeclaration, InspectionItem } from '../../types'; import { CustomsDeclaration, InspectionItem, InspectionIssue, CameraInfo } from '../../types';
import { useAppStore } from '../../store/useAppStore'; import { useAppStore } from '../../store/useAppStore';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
@@ -44,7 +44,6 @@ function InspectionContent() {
const { selectedCustoms, setSelectedCustoms } = useAppStore(); const { selectedCustoms, setSelectedCustoms } = useAppStore();
const [currentCustoms, setCurrentCustoms] = useState<CustomsDeclaration | null>(null); const [currentCustoms, setCurrentCustoms] = useState<CustomsDeclaration | null>(null);
const [loadingCustoms, setLoadingCustoms] = useState(true); const [loadingCustoms, setLoadingCustoms] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState<InspectionStatus>('idle'); const [status, setStatus] = useState<InspectionStatus>('idle');
const [logs, setLogs] = useState<{time: string, msg: string, type: 'info'|'warning'|'success'}[]>([]); const [logs, setLogs] = useState<{time: string, msg: string, type: 'info'|'warning'|'success'}[]>([]);
const [progressData, setProgressData] = useState<ProgressItem[]>([]); const [progressData, setProgressData] = useState<ProgressItem[]>([]);
@@ -52,7 +51,13 @@ function InspectionContent() {
const [pauseReason, setPauseReason] = useState(''); const [pauseReason, setPauseReason] = useState('');
const [customsList, setCustomsList] = useState<CustomsDeclaration[]>([]); const [customsList, setCustomsList] = useState<CustomsDeclaration[]>([]);
const [loadingList, setLoadingList] = useState(false); const [loadingList, setLoadingList] = useState(false);
const [currentView, setCurrentView] = useState<string>('摄像头1');
// Issues and cameras
const [issues, setIssues] = useState<InspectionIssue[]>([]);
const [cameras, setCameras] = useState<CameraInfo[]>([]);
// The segmented control options based on overview cameras
const [currentOverviewCamera, setCurrentOverviewCamera] = useState<string>('');
const { token } = theme.useToken(); const { token } = theme.useToken();
const logsEndRef = useRef<HTMLDivElement>(null); const logsEndRef = useRef<HTMLDivElement>(null);
@@ -63,6 +68,14 @@ function InspectionContent() {
setCustomsList(list); setCustomsList(list);
setLoadingList(false); setLoadingList(false);
}); });
MockApi.getCameraList().then(list => {
setCameras(list);
const overviews = list.filter(c => c.category === 'overview');
if (overviews.length > 0) {
setCurrentOverviewCamera(overviews[0].id);
}
});
MockApi.getInspectionIssues().then(list => setIssues(list));
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -70,7 +83,6 @@ function InspectionContent() {
const loadInspectionCustoms = async () => { const loadInspectionCustoms = async () => {
setLoadingCustoms(true); setLoadingCustoms(true);
setErrorMessage('');
try { try {
if (customsId) { if (customsId) {
@@ -81,7 +93,6 @@ function InspectionContent() {
if (!customs) { if (!customs) {
setCurrentCustoms(null); setCurrentCustoms(null);
setErrorMessage(`未找到报关单 ${customsId}`);
return; return;
} }
@@ -94,7 +105,6 @@ function InspectionContent() {
} catch { } catch {
if (!isMounted) return; if (!isMounted) return;
setCurrentCustoms(null); setCurrentCustoms(null);
setErrorMessage('查验任务加载失败,请稍后重试');
} finally { } finally {
if (isMounted) { if (isMounted) {
setLoadingCustoms(false); setLoadingCustoms(false);
@@ -195,6 +205,18 @@ function InspectionContent() {
}); });
}; };
const handleDisposeIssue = async (id: string) => {
await MockApi.disposeIssue(id);
setIssues(prev => prev.map(i => i.id === id ? { ...i, status: 'disposed' } : i));
addLog(`已处置异常: ${id}`, 'success');
};
const handleCancelIssue = async (id: string) => {
await MockApi.cancelIssue(id);
setIssues(prev => prev.map(i => i.id === id ? { ...i, status: 'cancelled' } : i));
addLog(`已取消异常: ${id}`, 'info');
};
const calculateTotalProgress = () => { const calculateTotalProgress = () => {
if (!progressData.length) return 0; if (!progressData.length) return 0;
const total = progressData.reduce((acc, curr) => acc + curr.quantify, 0); const total = progressData.reduce((acc, curr) => acc + curr.quantify, 0);
@@ -202,6 +224,61 @@ function InspectionContent() {
return Math.round((inspected / total) * 100); return Math.round((inspected / total) * 100);
}; };
const overviewCameras = cameras.filter(c => c.category === 'overview');
const agvCamera = cameras.find(c => c.category === 'agv');
const operationCamera = cameras.find(c => c.category === 'operation');
const selectedOverviewCamera = overviewCameras.find(c => c.id === currentOverviewCamera) || overviewCameras[0];
const issueColumns: ColumnsType<InspectionIssue> = [
{
title: '时间',
dataIndex: 'time',
key: 'time',
width: 90,
render: (text) => <Text style={{ fontSize: 13 }}>{text}</Text>
},
{
title: '问题描述',
dataIndex: 'description',
key: 'description',
render: (text, record) => (
<Space>
<Badge status={record.severity === 'error' ? 'error' : 'warning'} />
<Text style={{ fontSize: 13 }}>{text}</Text>
</Space>
)
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
render: (status) => {
const map = {
pending: { color: 'red', text: '待处理' },
disposed: { color: 'green', text: '已处置' },
cancelled: { color: 'default', text: '已取消' }
};
const info = map[status as keyof typeof map];
return <Tag color={info.color} style={{ margin: 0 }}>{info.text}</Tag>;
}
},
{
title: '操作',
key: 'action',
width: 140,
render: (_, record) => (
record.status === 'pending' ? (
<Space size="small">
<Button size="small" type="primary" onClick={() => handleDisposeIssue(record.id)}></Button>
<Button size="small" onClick={() => handleCancelIssue(record.id)}></Button>
</Space>
) : null
),
},
];
if (loadingCustoms) { if (loadingCustoms) {
return ( return (
<Flex vertical align="center" justify="center" style={{ padding: 48, height: '100vh' }}> <Flex vertical align="center" justify="center" style={{ padding: 48, height: '100vh' }}>
@@ -212,72 +289,16 @@ function InspectionContent() {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)' }}> <div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)' }}>
{/* 顶部工具条:面包屑 + 报关单选择器 + 状态指示灯 */}
<Flex align="center" justify="space-between" style={{ padding: '0 24px', margin: '16px 0' }}>
<Breadcrumb /> <Breadcrumb />
<Flex align="center" gap="large">
<Row gutter={24} style={{ flex: 1, minHeight: 0, margin: '0 24px 24px 24px' }}> {currentCustoms && (
{/* 左侧:AGV 及监控画面 */} <Badge
<Col span={14}> status={status === 'running' ? 'processing' : status === 'idle' ? 'default' : status === 'paused' ? 'warning' : 'success'}
<Card text={<Text strong>{status === 'running' ? '作业中' : status === 'idle' ? '待作业' : status === 'paused' ? '已暂停' : '已完成'}</Text>}
title={
<Flex justify="space-between" align="center">
<Space>
<VideoCameraOutlined style={{ color: token.colorPrimary }} />
<span>AGV </span>
</Space>
<Space>
<Badge status="processing" text="设备在线" />
</Space>
</Flex>
}
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', flex: 1 } }}
style={{ height: '100%', display: 'flex', flexDirection: 'column', borderRadius: token.borderRadiusLG, overflow: 'hidden' }}
bordered={false}
>
<div
style={{
position: 'relative',
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
background: '#000000'
}}
>
{status === 'running' ? (
<Flex vertical align="center" gap={16} style={{ color: '#ffffff' }}>
<CaretRightOutlined style={{ fontSize: 48, color: token.colorPrimary, opacity: 0.8 }} />
<Text style={{ color: '#fff' }}>...</Text>
<div style={{ padding: '8px 16px', border: `1px dashed ${token.colorPrimary}`, borderRadius: token.borderRadius }}>
<span style={{ color: token.colorPrimary }}>AI </span>
</div>
</Flex>
) : (
<Flex vertical align="center" gap={16} style={{ color: token.colorTextDescription }}>
<PauseCircleFilled style={{ fontSize: 48 }} />
<Text type="secondary"></Text>
</Flex>
)}
</div>
<div style={{ padding: '16px 24px', borderTop: `1px solid ${token.colorBorderSecondary}`, background: token.colorFillAlter }}>
<Flex align="center" gap="middle">
<Text strong>:</Text>
<Segmented
options={['摄像头1', '摄像头2', '摄像头3', '摄像头4', '摄像头5']}
value={currentView}
onChange={setCurrentView}
/> />
</Flex> )}
</div>
</Card>
</Col>
{/* 右侧:查验控制面板 */}
<Col span={10}>
<Card
title={
<Flex justify="space-between" align="center">
<span></span>
<Select <Select
showSearch showSearch
placeholder="搜索并选择报关单..." placeholder="搜索并选择报关单..."
@@ -297,26 +318,94 @@ function InspectionContent() {
}} }}
/> />
</Flex> </Flex>
} </Flex>
styles={{ body: { overflow: 'hidden', padding: '24px', display: 'flex', flexDirection: 'column', flex: 1 } }}
style={{ height: '100%', display: 'flex', flexDirection: 'column', borderRadius: token.borderRadiusLG }} <Row gutter={24} style={{ flex: 1, minHeight: 0, margin: '0 24px 24px 24px' }}>
bordered={false} {/* 左侧:多摄像头 + 控制区 */}
<Col span={16} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* 三画面同框区 */}
<div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 16, marginBottom: 16 }}>
{/* 左列 (1/3) */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* AGV 主视角 */}
<Card
size="small"
title={<Text strong>AGV ({agvCamera?.name || '未知'})</Text>}
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column' } }}
style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
> >
{!currentCustoms ? ( <div style={{ flex: 1, background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', position: 'relative' }}>
<Flex vertical align="center" justify="center" style={{ flex: 1 }}> {status === 'running' ? (
{errorMessage && ( <Flex vertical align="center" gap={16}>
<Alert <CaretRightOutlined style={{ fontSize: 48, color: token.colorPrimary, opacity: 0.8 }} />
type="error" <Text style={{ color: '#fff' }}>...</Text>
message={errorMessage} <div style={{ padding: '8px 16px', border: `1px dashed ${token.colorPrimary}`, borderRadius: token.borderRadius }}>
showIcon <span style={{ color: token.colorPrimary }}>AI </span>
style={{ marginBottom: 24, width: '100%' }} </div>
/>
)}
<Empty description="请在右上角选择要查验的报关单" />
</Flex> </Flex>
) : ( ) : (
<> <Flex vertical align="center" gap={16} style={{ color: token.colorTextDescription }}>
<Flex justify="center" gap="middle" style={{ marginBottom: 32 }}> <PauseCircleFilled style={{ fontSize: 48 }} />
<Text type="secondary"></Text>
</Flex>
)}
</div>
</Card>
{/* 作业视角 */}
<Card
size="small"
title={<Text strong> ({operationCamera?.name || '未知'})</Text>}
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column' } }}
style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
>
<div style={{ flex: 1, background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
{status === 'running' ? (
<Text style={{ color: '#fff' }}>...</Text>
) : (
<Text type="secondary"></Text>
)}
</div>
</Card>
</div>
{/* 右列 (2/3) 查验区监控摄像头 */}
<Card
size="small"
title={
<Flex justify="space-between" align="center">
<Text strong> ({selectedOverviewCamera?.name || '未知'})</Text>
<Button
size="small"
onClick={() => {
if (overviewCameras.length > 0) {
const currentIndex = overviewCameras.findIndex(c => c.id === currentOverviewCamera);
const nextIndex = (currentIndex + 1) % overviewCameras.length;
setCurrentOverviewCamera(overviewCameras[nextIndex]?.id || currentOverviewCamera);
}
}}
>
</Button>
</Flex>
}
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column' } }}
style={{ flex: 2, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
>
<div style={{ flex: 1, background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
{status === 'running' ? (
<Text style={{ color: '#fff' }}>...</Text>
) : (
<Text type="secondary"></Text>
)}
</div>
</Card>
</div>
{/* 控制按钮区 */}
<Card size="small" style={{ flexShrink: 0 }}>
<Flex justify="center" gap="middle">
{status === 'idle' || status === 'paused' ? ( {status === 'idle' || status === 'paused' ? (
<Button type="primary" size="large" icon={<PlayCircleOutlined />} onClick={handleStart} style={{ width: 140 }}> <Button type="primary" size="large" icon={<PlayCircleOutlined />} onClick={handleStart} style={{ width: 140 }}>
{status === 'idle' ? '开始查验' : '继续查验'} {status === 'idle' ? '开始查验' : '继续查验'}
@@ -329,23 +418,33 @@ function InspectionContent() {
<Button size="large" icon={<ReloadOutlined />} disabled={status === 'completed'}></Button> <Button size="large" icon={<ReloadOutlined />} disabled={status === 'completed'}></Button>
<Button danger size="large" icon={<StopOutlined />} onClick={handleEnd} disabled={status === 'completed' || status === 'idle'}></Button> <Button danger size="large" icon={<StopOutlined />} onClick={handleEnd} disabled={status === 'completed' || status === 'idle'}></Button>
</Flex> </Flex>
</Card>
</Col>
<Divider titlePlacement="start" plain style={{ margin: '0 0 16px 0' }}><Text strong></Text></Divider> {/* 右侧:信息面板 */}
<div style={{ marginBottom: 16, padding: '0 8px' }}> <Col span={8} style={{ display: 'flex', flexDirection: 'column', gap: 16, height: '100%' }}>
{/* 核销进度 */}
<Card
title={<Text strong></Text>}
size="small"
style={{ flex: 4, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
styles={{ body: { padding: '12px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 } }}
>
<div style={{ marginBottom: 16 }}>
<Progress percent={calculateTotalProgress()} status={status === 'completed' ? 'success' : 'active'} strokeWidth={10} /> <Progress percent={calculateTotalProgress()} status={status === 'completed' ? 'success' : 'active'} strokeWidth={10} />
</div> </div>
<div style={{ flex: 1, overflowY: 'auto' }}>
<div style={{ flex: '0 1 35%', marginBottom: 24, overflowY: 'auto', paddingRight: 8 }}>
<List <List
size="small" size="small"
dataSource={progressData} dataSource={progressData}
renderItem={item => ( renderItem={item => (
<List.Item style={{ padding: '12px 8px', borderBottom: `1px solid ${token.colorBorderSecondary}` }}> <List.Item style={{ padding: '8px 0', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}> <Flex justify="space-between" align="center" style={{ marginBottom: 4 }}>
<Text strong>{item.inventoryName}</Text> <Text strong style={{ fontSize: 13 }}>{item.inventoryName}</Text>
<Space> <Space>
<Text type="secondary" style={{ fontSize: 13 }}>{item.inventoryCode}</Text> <Text type="secondary" style={{ fontSize: 12 }}>{item.inventoryCode}</Text>
<Badge count={`${item.currentInspected} / ${item.quantify}`} style={{ backgroundColor: item.currentInspected === item.quantify ? token.colorSuccess : token.colorPrimary }} /> <Badge count={`${item.currentInspected} / ${item.quantify}`} style={{ backgroundColor: item.currentInspected === item.quantify ? token.colorSuccess : token.colorPrimary }} />
</Space> </Space>
</Flex> </Flex>
@@ -355,16 +454,35 @@ function InspectionContent() {
)} )}
/> />
</div> </div>
</Card>
<Divider titlePlacement="start" plain style={{ margin: '0 0 16px 0' }}><Text strong></Text></Divider> {/* 查验异常 */}
<div style={{ <Card
flex: 1, title={<Text strong></Text>}
overflowY: 'auto', size="small"
border: `1px solid ${token.colorBorderSecondary}`, style={{ flex: 3, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
borderRadius: token.borderRadiusLG, styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 } }}
background: token.colorFillQuaternary, >
padding: 16 <div style={{ flex: 1, overflowY: 'auto' }}>
}}> <Table
columns={issueColumns}
dataSource={issues}
rowKey="id"
size="small"
pagination={false}
sticky
/>
</div>
</Card>
{/* 查验日志 */}
<Card
title={<Text strong></Text>}
size="small"
style={{ flex: 3, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
styles={{ body: { padding: '12px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, background: token.colorFillQuaternary } }}
>
<div style={{ flex: 1, overflowY: 'auto', paddingRight: 8 }}>
{logs.length > 0 ? ( {logs.length > 0 ? (
<Timeline <Timeline
items={logs.map((item) => ({ items={logs.map((item) => ({
@@ -372,7 +490,7 @@ function InspectionContent() {
children: ( children: (
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary" style={{ fontSize: 12 }}>{item.time}</Text> <Text type="secondary" style={{ fontSize: 12 }}>{item.time}</Text>
<Text>{item.msg}</Text> <Text style={{ fontSize: 13 }}>{item.msg}</Text>
</Space> </Space>
), ),
}))} }))}
@@ -382,9 +500,8 @@ function InspectionContent() {
)} )}
<div ref={logsEndRef} /> <div ref={logsEndRef} />
</div> </div>
</>
)}
</Card> </Card>
</Col> </Col>
</Row> </Row>
+23 -14
View File
@@ -100,9 +100,9 @@ export default function DashboardPage() {
]; ];
const quickActions = [ const quickActions = [
{ title: '扫码查询机器', desc: '使用平板摄像头扫描设备二维码', icon: <ScanOutlined />, onClick: () => router.push('/machines') }, { title: '扫码查询机器', desc: '使用平板摄像头扫描设备二维码', icon: <ScanOutlined />, onClick: () => router.push('/machines'), color: '#1890ff' },
{ title: '序列号查询机器', desc: '手动输入序列号查询机器全部资料', icon: <SearchOutlined />, onClick: () => router.push('/machines') }, { title: '序列号查询机器', desc: '手动输入序列号查询机器全部资料', icon: <SearchOutlined />, onClick: () => router.push('/machines'), color: '#1890ff' },
{ title: '视频监控', desc: '查看厂房实时监控画面', icon: <VideoCameraOutlined />, onClick: () => router.push('/video') }, { title: '视频监控', desc: '查看厂房实时监控画面', icon: <VideoCameraOutlined />, onClick: () => router.push('/video'), color: '#1890ff' },
]; ];
return ( return (
@@ -135,39 +135,48 @@ export default function DashboardPage() {
</Row> </Row>
{/* 快捷操作区域 */} {/* 快捷操作区域 */}
<Row gutter={24} style={{ marginBottom: 24 }}> <div style={{ marginBottom: 32, marginTop: 16 }}>
<div style={{ fontSize: 18, marginBottom: 16, fontWeight: 600, color: '#333' }}></div>
<Row gutter={24}>
{quickActions.map((action, idx) => ( {quickActions.map((action, idx) => (
<Col span={8} key={idx}> <Col span={8} key={idx}>
<Card <Card
hoverable hoverable
onClick={action.onClick} onClick={action.onClick}
styles={{ body: { background: 'linear-gradient(135deg, #f6f8fc 0%, #eef2f9 100%)' } }} style={{
borderRadius: 8,
overflow: 'hidden',
border: '1px solid #f0f0f0',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
}}
styles={{ body: { padding: '24px', background: '#fff', height: '100%' } }}
> >
<Flex align="center" gap={16}> <Flex align="center" gap={20}>
<Flex <Flex
align="center" align="center"
justify="center" justify="center"
style={{ style={{
width: 56, width: 56,
height: 56, height: 56,
borderRadius: '50%', borderRadius: 8,
background: '#fff', background: '#f0f5ff',
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.1)', color: action.color,
color: '#1890ff', fontSize: 28,
fontSize: 24 flexShrink: 0
}} }}
> >
{action.icon} {action.icon}
</Flex> </Flex>
<Flex vertical> <Flex vertical gap={4} style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 18 }}>{action.title}</div> <div style={{ fontWeight: 600, fontSize: 18, color: '#262626' }}>{action.title}</div>
<div style={{ color: 'var(--color-text-secondary)', fontSize: 14 }}>{action.desc}</div> <div style={{ color: '#595959', fontSize: 13, lineHeight: 1.5 }}>{action.desc}</div>
</Flex> </Flex>
</Flex> </Flex>
</Card> </Card>
</Col> </Col>
))} ))}
</Row> </Row>
</div>
<Row gutter={24}> <Row gutter={24}>
{/* 最近查验动态 */} {/* 最近查验动态 */}
@@ -3,7 +3,8 @@ import {
ActivityItem, ActivityItem,
CustomsDeclaration, CustomsDeclaration,
CameraInfo, CameraInfo,
MachineDetail MachineDetail,
InspectionIssue
} from '../types'; } from '../types';
const customsDeclarations: CustomsDeclaration[] = [ const customsDeclarations: CustomsDeclaration[] = [
@@ -76,14 +77,36 @@ export const MockApi = {
getCameraList: async (): Promise<CameraInfo[]> => { getCameraList: async (): Promise<CameraInfo[]> => {
await delay(300); await delay(300);
return [ return [
{ id: '1', name: '摄像头 1', location: '入料口', streamUrl: '', status: 'online' }, { id: '1', name: '监控摄像头 1', location: '查验区东侧', streamUrl: '', status: 'online', category: 'overview' },
{ id: '2', name: '摄像头 2', location: '生产线 A', streamUrl: '', status: 'online' }, { id: '2', name: '监控摄像头 2', location: '查验区南侧', streamUrl: '', status: 'online', category: 'overview' },
{ id: '3', name: '摄像头 3', location: '生产线 B', streamUrl: '', status: 'online' }, { id: '3', name: '监控摄像头 3', location: '查验区西侧', streamUrl: '', status: 'online', category: 'overview' },
{ id: '4', name: '摄像头 4', location: '出库区', streamUrl: '', status: 'offline' }, { id: '4', name: '监控摄像头 4', location: '查验区北侧', streamUrl: '', status: 'online', category: 'overview' },
{ id: '5', name: '摄像头 5', location: 'AGV 作业区', streamUrl: '', status: 'online' }, { id: '5', name: 'AGV 主摄像头', location: 'AGV 前端', streamUrl: '', status: 'online', category: 'agv' },
{ id: '6', name: '作业视角', location: '机械臂', streamUrl: '', status: 'online', category: 'operation' },
]; ];
}, },
getInspectionIssues: async (): Promise<InspectionIssue[]> => {
await delay(300);
return [
{ id: 'issue-1', time: '14:25:30', description: '序列号不匹配', severity: 'error', status: 'pending' },
{ id: 'issue-2', time: '14:26:15', description: '外包装破损', severity: 'warning', status: 'pending' },
{ id: 'issue-3', time: '14:20:00', description: '数量缺少 1 件', severity: 'error', status: 'disposed', disposedAt: '14:22:10', disposedBy: '系统自动' },
];
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
disposeIssue: async (_id: string): Promise<boolean> => {
await delay(200);
return true;
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cancelIssue: async (_id: string): Promise<boolean> => {
await delay(200);
return true;
},
getMachineDetail: async (serialNumber: string): Promise<MachineDetail> => { getMachineDetail: async (serialNumber: string): Promise<MachineDetail> => {
await delay(400); await delay(400);
return { return {
@@ -59,6 +59,17 @@ export interface CameraInfo {
streamUrl: string; streamUrl: string;
status: 'online' | 'offline'; status: 'online' | 'offline';
snapshot?: string; snapshot?: string;
category?: 'overview' | 'agv' | 'operation';
}
export interface InspectionIssue {
id: string;
time: string;
description: string;
severity: 'warning' | 'error';
status: 'pending' | 'disposed' | 'cancelled';
disposedAt?: string;
disposedBy?: string;
} }
export interface CustomsDeclaration { export interface CustomsDeclaration {