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';
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 {
PlayCircleOutlined,
PauseCircleOutlined,
StopOutlined,
ReloadOutlined,
CaretRightOutlined,
PauseCircleFilled,
VideoCameraOutlined
PauseCircleFilled
} from '@ant-design/icons';
import { Breadcrumb } from '../../components/Breadcrumb';
import { MockApi } from '../../services/mockApi';
import { CustomsDeclaration, InspectionItem } from '../../types';
import { CustomsDeclaration, InspectionItem, InspectionIssue, CameraInfo } from '../../types';
import { useAppStore } from '../../store/useAppStore';
import { useRouter, useSearchParams } from 'next/navigation';
@@ -44,7 +44,6 @@ function InspectionContent() {
const { selectedCustoms, setSelectedCustoms } = useAppStore();
const [currentCustoms, setCurrentCustoms] = useState<CustomsDeclaration | null>(null);
const [loadingCustoms, setLoadingCustoms] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState<InspectionStatus>('idle');
const [logs, setLogs] = useState<{time: string, msg: string, type: 'info'|'warning'|'success'}[]>([]);
const [progressData, setProgressData] = useState<ProgressItem[]>([]);
@@ -52,8 +51,14 @@ function InspectionContent() {
const [pauseReason, setPauseReason] = useState('');
const [customsList, setCustomsList] = useState<CustomsDeclaration[]>([]);
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 logsEndRef = useRef<HTMLDivElement>(null);
@@ -63,6 +68,14 @@ function InspectionContent() {
setCustomsList(list);
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(() => {
@@ -70,7 +83,6 @@ function InspectionContent() {
const loadInspectionCustoms = async () => {
setLoadingCustoms(true);
setErrorMessage('');
try {
if (customsId) {
@@ -81,7 +93,6 @@ function InspectionContent() {
if (!customs) {
setCurrentCustoms(null);
setErrorMessage(`未找到报关单 ${customsId}`);
return;
}
@@ -94,7 +105,6 @@ function InspectionContent() {
} catch {
if (!isMounted) return;
setCurrentCustoms(null);
setErrorMessage('查验任务加载失败,请稍后重试');
} finally {
if (isMounted) {
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 = () => {
if (!progressData.length) return 0;
const total = progressData.reduce((acc, curr) => acc + curr.quantify, 0);
@@ -202,6 +224,61 @@ function InspectionContent() {
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) {
return (
<Flex vertical align="center" justify="center" style={{ padding: 48, height: '100vh' }}>
@@ -212,179 +289,219 @@ function InspectionContent() {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)' }}>
<Breadcrumb />
{/* 顶部工具条:面包屑 + 报关单选择器 + 状态指示灯 */}
<Flex align="center" justify="space-between" style={{ padding: '0 24px', margin: '16px 0' }}>
<Breadcrumb />
<Flex align="center" gap="large">
{currentCustoms && (
<Badge
status={status === 'running' ? 'processing' : status === 'idle' ? 'default' : status === 'paused' ? 'warning' : 'success'}
text={<Text strong>{status === 'running' ? '作业中' : status === 'idle' ? '待作业' : status === 'paused' ? '已暂停' : '已完成'}</Text>}
/>
)}
<Select
showSearch
placeholder="搜索并选择报关单..."
style={{ width: 240 }}
loading={loadingList}
optionFilterProp="label"
filterOption={(input, option) =>
(option?.label as string ?? '').toLowerCase().includes(input.toLowerCase())
}
options={customsList.map(item => ({
value: item.customsId,
label: `${item.customsId} - ${item.status === 'pending' ? '待查验' : item.status === 'inspecting' ? '查验中' : '已放行'}`
}))}
value={currentCustoms?.customsId || undefined}
onChange={(value) => {
router.push(`/inspection?customsId=${value}`);
}}
/>
</Flex>
</Flex>
<Row gutter={24} style={{ flex: 1, minHeight: 0, margin: '0 24px 24px 24px' }}>
{/* 左侧:AGV 及监控画面 */}
<Col span={14}>
<Card
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>
)}
{/* 左侧:多摄像头 + 控制区 */}
<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' }}
>
<div style={{ flex: 1, background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', position: 'relative' }}>
{status === 'running' ? (
<Flex vertical align="center" gap={16}>
<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>
</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>
<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>
{/* 右列 (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' ? (
<Button type="primary" size="large" icon={<PlayCircleOutlined />} onClick={handleStart} style={{ width: 140 }}>
{status === 'idle' ? '开始查验' : '继续查验'}
</Button>
) : status === 'running' ? (
<Button type="primary" danger size="large" icon={<PauseCircleOutlined />} onClick={handlePause} style={{ width: 140 }}>
</Button>
) : null}
<Button size="large" icon={<ReloadOutlined />} disabled={status === 'completed'}></Button>
<Button danger size="large" icon={<StopOutlined />} onClick={handleEnd} disabled={status === 'completed' || status === 'idle'}></Button>
</Flex>
</Card>
</Col>
{/* 右侧:查验控制面板 */}
<Col span={10}>
<Card
title={
<Flex justify="space-between" align="center">
<span></span>
<Select
showSearch
placeholder="搜索并选择报关单..."
style={{ width: 240 }}
loading={loadingList}
optionFilterProp="label"
filterOption={(input, option) =>
(option?.label as string ?? '').toLowerCase().includes(input.toLowerCase())
}
options={customsList.map(item => ({
value: item.customsId,
label: `${item.customsId} - ${item.status === 'pending' ? '待查验' : item.status === 'inspecting' ? '查验中' : '已放行'}`
}))}
value={currentCustoms?.customsId || undefined}
onChange={(value) => {
router.push(`/inspection?customsId=${value}`);
}}
/>
</Flex>
}
styles={{ body: { overflow: 'hidden', padding: '24px', display: 'flex', flexDirection: 'column', flex: 1 } }}
style={{ height: '100%', display: 'flex', flexDirection: 'column', borderRadius: token.borderRadiusLG }}
bordered={false}
{/* 右侧:信息面板 */}
<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 } }}
>
{!currentCustoms ? (
<Flex vertical align="center" justify="center" style={{ flex: 1 }}>
{errorMessage && (
<Alert
type="error"
message={errorMessage}
showIcon
style={{ marginBottom: 24, width: '100%' }}
/>
<div style={{ marginBottom: 16 }}>
<Progress percent={calculateTotalProgress()} status={status === 'completed' ? 'success' : 'active'} strokeWidth={10} />
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
<List
size="small"
dataSource={progressData}
renderItem={item => (
<List.Item style={{ padding: '8px 0', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
<div style={{ width: '100%' }}>
<Flex justify="space-between" align="center" style={{ marginBottom: 4 }}>
<Text strong style={{ fontSize: 13 }}>{item.inventoryName}</Text>
<Space>
<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 }} />
</Space>
</Flex>
<Progress percent={Math.round((item.currentInspected / item.quantify) * 100)} showInfo={false} size="small" />
</div>
</List.Item>
)}
<Empty description="请在右上角选择要查验的报关单" />
</Flex>
) : (
<>
<Flex justify="center" gap="middle" style={{ marginBottom: 32 }}>
{status === 'idle' || status === 'paused' ? (
<Button type="primary" size="large" icon={<PlayCircleOutlined />} onClick={handleStart} style={{ width: 140 }}>
{status === 'idle' ? '开始查验' : '继续查验'}
</Button>
) : status === 'running' ? (
<Button type="primary" danger size="large" icon={<PauseCircleOutlined />} onClick={handlePause} style={{ width: 140 }}>
</Button>
) : null}
<Button size="large" icon={<ReloadOutlined />} disabled={status === 'completed'}></Button>
<Button danger size="large" icon={<StopOutlined />} onClick={handleEnd} disabled={status === 'completed' || status === 'idle'}></Button>
</Flex>
<Divider titlePlacement="start" plain style={{ margin: '0 0 16px 0' }}><Text strong></Text></Divider>
<div style={{ marginBottom: 16, padding: '0 8px' }}>
<Progress percent={calculateTotalProgress()} status={status === 'completed' ? 'success' : 'active'} strokeWidth={10} />
</div>
<div style={{ flex: '0 1 35%', marginBottom: 24, overflowY: 'auto', paddingRight: 8 }}>
<List
size="small"
dataSource={progressData}
renderItem={item => (
<List.Item style={{ padding: '12px 8px', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
<div style={{ width: '100%' }}>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
<Text strong>{item.inventoryName}</Text>
<Space>
<Text type="secondary" style={{ fontSize: 13 }}>{item.inventoryCode}</Text>
<Badge count={`${item.currentInspected} / ${item.quantify}`} style={{ backgroundColor: item.currentInspected === item.quantify ? token.colorSuccess : token.colorPrimary }} />
</Space>
</Flex>
<Progress percent={Math.round((item.currentInspected / item.quantify) * 100)} showInfo={false} size="small" />
</div>
</List.Item>
)}
/>
</div>
<Divider titlePlacement="start" plain style={{ margin: '0 0 16px 0' }}><Text strong></Text></Divider>
<div style={{
flex: 1,
overflowY: 'auto',
border: `1px solid ${token.colorBorderSecondary}`,
borderRadius: token.borderRadiusLG,
background: token.colorFillQuaternary,
padding: 16
}}>
{logs.length > 0 ? (
<Timeline
items={logs.map((item) => ({
color: item.type === 'success' ? 'green' : item.type === 'warning' ? 'orange' : 'blue',
children: (
<Space direction="vertical" size={0}>
<Text type="secondary" style={{ fontSize: 12 }}>{item.time}</Text>
<Text>{item.msg}</Text>
</Space>
),
}))}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无日志" style={{ margin: '20px 0' }} />
)}
<div ref={logsEndRef} />
</div>
</>
)}
/>
</div>
</Card>
{/* 查验异常 */}
<Card
title={<Text strong></Text>}
size="small"
style={{ flex: 3, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 } }}
>
<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 ? (
<Timeline
items={logs.map((item) => ({
color: item.type === 'success' ? 'green' : item.type === 'warning' ? 'orange' : 'blue',
children: (
<Space direction="vertical" size={0}>
<Text type="secondary" style={{ fontSize: 12 }}>{item.time}</Text>
<Text style={{ fontSize: 13 }}>{item.msg}</Text>
</Space>
),
}))}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无日志" style={{ margin: '20px 0' }} />
)}
<div ref={logsEndRef} />
</div>
</Card>
</Col>
</Row>
+44 -35
View File
@@ -100,9 +100,9 @@ export default function DashboardPage() {
];
const quickActions = [
{ title: '扫码查询机器', desc: '使用平板摄像头扫描设备二维码', icon: <ScanOutlined />, onClick: () => router.push('/machines') },
{ title: '序列号查询机器', desc: '手动输入序列号查询机器全部资料', icon: <SearchOutlined />, onClick: () => router.push('/machines') },
{ title: '视频监控', desc: '查看厂房实时监控画面', icon: <VideoCameraOutlined />, onClick: () => router.push('/video') },
{ title: '扫码查询机器', desc: '使用平板摄像头扫描设备二维码', icon: <ScanOutlined />, onClick: () => router.push('/machines'), color: '#1890ff' },
{ title: '序列号查询机器', desc: '手动输入序列号查询机器全部资料', icon: <SearchOutlined />, onClick: () => router.push('/machines'), color: '#1890ff' },
{ title: '视频监控', desc: '查看厂房实时监控画面', icon: <VideoCameraOutlined />, onClick: () => router.push('/video'), color: '#1890ff' },
];
return (
@@ -135,39 +135,48 @@ export default function DashboardPage() {
</Row>
{/* 快捷操作区域 */}
<Row gutter={24} style={{ marginBottom: 24 }}>
{quickActions.map((action, idx) => (
<Col span={8} key={idx}>
<Card
hoverable
onClick={action.onClick}
styles={{ body: { background: 'linear-gradient(135deg, #f6f8fc 0%, #eef2f9 100%)' } }}
>
<Flex align="center" gap={16}>
<Flex
align="center"
justify="center"
style={{
width: 56,
height: 56,
borderRadius: '50%',
background: '#fff',
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.1)',
color: '#1890ff',
fontSize: 24
}}
>
{action.icon}
<div style={{ marginBottom: 32, marginTop: 16 }}>
<div style={{ fontSize: 18, marginBottom: 16, fontWeight: 600, color: '#333' }}></div>
<Row gutter={24}>
{quickActions.map((action, idx) => (
<Col span={8} key={idx}>
<Card
hoverable
onClick={action.onClick}
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={20}>
<Flex
align="center"
justify="center"
style={{
width: 56,
height: 56,
borderRadius: 8,
background: '#f0f5ff',
color: action.color,
fontSize: 28,
flexShrink: 0
}}
>
{action.icon}
</Flex>
<Flex vertical gap={4} style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 18, color: '#262626' }}>{action.title}</div>
<div style={{ color: '#595959', fontSize: 13, lineHeight: 1.5 }}>{action.desc}</div>
</Flex>
</Flex>
<Flex vertical>
<div style={{ fontWeight: 600, fontSize: 18 }}>{action.title}</div>
<div style={{ color: 'var(--color-text-secondary)', fontSize: 14 }}>{action.desc}</div>
</Flex>
</Flex>
</Card>
</Col>
))}
</Row>
</Card>
</Col>
))}
</Row>
</div>
<Row gutter={24}>
{/* 最近查验动态 */}
@@ -3,7 +3,8 @@ import {
ActivityItem,
CustomsDeclaration,
CameraInfo,
MachineDetail
MachineDetail,
InspectionIssue
} from '../types';
const customsDeclarations: CustomsDeclaration[] = [
@@ -76,14 +77,36 @@ export const MockApi = {
getCameraList: async (): Promise<CameraInfo[]> => {
await delay(300);
return [
{ id: '1', name: '摄像头 1', location: '入料口', streamUrl: '', status: 'online' },
{ id: '2', name: '摄像头 2', location: '生产线 A', streamUrl: '', status: 'online' },
{ id: '3', name: '摄像头 3', location: '生产线 B', streamUrl: '', status: 'online' },
{ id: '4', name: '摄像头 4', location: '出库区', streamUrl: '', status: 'offline' },
{ id: '5', name: '摄像头 5', location: 'AGV 作业区', streamUrl: '', status: 'online' },
{ id: '1', name: '监控摄像头 1', location: '查验区东侧', streamUrl: '', status: 'online', category: 'overview' },
{ id: '2', name: '监控摄像头 2', location: '查验区南侧', streamUrl: '', status: 'online', category: 'overview' },
{ id: '3', name: '监控摄像头 3', location: '查验区西侧', streamUrl: '', status: 'online', category: 'overview' },
{ id: '4', name: '监控摄像头 4', location: '查验区北侧', streamUrl: '', status: 'online', category: 'overview' },
{ 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> => {
await delay(400);
return {
@@ -59,6 +59,17 @@ export interface CameraInfo {
streamUrl: string;
status: 'online' | 'offline';
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 {