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,179 +289,219 @@ function InspectionContent() {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)' }}> <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' }}> <Row gutter={24} style={{ flex: 1, minHeight: 0, margin: '0 24px 24px 24px' }}>
{/* 左侧:AGV 及监控画面 */} {/* 左侧:多摄像头 + 控制区 */}
<Col span={14}> <Col span={16} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Card
title={ {/* 三画面同框区 */}
<Flex justify="space-between" align="center"> <div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 16, marginBottom: 16 }}>
<Space> {/* 左列 (1/3) */}
<VideoCameraOutlined style={{ color: token.colorPrimary }} /> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 16 }}>
<span>AGV </span> {/* AGV 主视角 */}
</Space> <Card
<Space> size="small"
<Badge status="processing" text="设备在线" /> title={<Text strong>AGV ({agvCamera?.name || '未知'})</Text>}
</Space> styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column' } }}
</Flex> style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
} >
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', flex: 1 } }} <div style={{ flex: 1, background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', position: 'relative' }}>
style={{ height: '100%', display: 'flex', flexDirection: 'column', borderRadius: token.borderRadiusLG, overflow: 'hidden' }} {status === 'running' ? (
bordered={false} <Flex vertical align="center" gap={16}>
> <CaretRightOutlined style={{ fontSize: 48, color: token.colorPrimary, opacity: 0.8 }} />
<div <Text style={{ color: '#fff' }}>...</Text>
style={{ <div style={{ padding: '8px 16px', border: `1px dashed ${token.colorPrimary}`, borderRadius: token.borderRadius }}>
position: 'relative', <span style={{ color: token.colorPrimary }}>AI </span>
display: 'flex', </div>
flex: 1, </Flex>
alignItems: 'center', ) : (
justifyContent: 'center', <Flex vertical align="center" gap={16} style={{ color: token.colorTextDescription }}>
background: '#000000' <PauseCircleFilled style={{ fontSize: 48 }} />
}} <Text type="secondary"></Text>
> </Flex>
{status === 'running' ? ( )}
<Flex vertical align="center" gap={16} style={{ color: '#ffffff' }}> </div>
<CaretRightOutlined style={{ fontSize: 48, color: token.colorPrimary, opacity: 0.8 }} /> </Card>
<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> <Card
</div> size="small"
</Flex> title={<Text strong> ({operationCamera?.name || '未知'})</Text>}
) : ( styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column' } }}
<Flex vertical align="center" gap={16} style={{ color: token.colorTextDescription }}> style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
<PauseCircleFilled style={{ fontSize: 48 }} /> >
<Text type="secondary"></Text> <div style={{ flex: 1, background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
</Flex> {status === 'running' ? (
)} <Text style={{ color: '#fff' }}>...</Text>
) : (
<Text type="secondary"></Text>
)}
</div>
</Card>
</div> </div>
<div style={{ padding: '16px 24px', borderTop: `1px solid ${token.colorBorderSecondary}`, background: token.colorFillAlter }}> {/* 右列 (2/3) 查验区监控摄像头 */}
<Flex align="center" gap="middle"> <Card
<Text strong>:</Text> size="small"
<Segmented title={
options={['摄像头1', '摄像头2', '摄像头3', '摄像头4', '摄像头5']} <Flex justify="space-between" align="center">
value={currentView} <Text strong> ({selectedOverviewCamera?.name || '未知'})</Text>
onChange={setCurrentView} <Button
/> size="small"
</Flex> onClick={() => {
</div> 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> </Card>
</Col> </Col>
{/* 右侧:查验控制面板 */} {/* 右侧:信息面板 */}
<Col span={10}> <Col span={8} style={{ display: 'flex', flexDirection: 'column', gap: 16, height: '100%' }}>
{/* 核销进度 */}
<Card <Card
title={ title={<Text strong></Text>}
<Flex justify="space-between" align="center"> size="small"
<span></span> style={{ flex: 4, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
<Select styles={{ body: { padding: '12px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 } }}
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}
> >
{!currentCustoms ? ( <div style={{ marginBottom: 16 }}>
<Flex vertical align="center" justify="center" style={{ flex: 1 }}> <Progress percent={calculateTotalProgress()} status={status === 'completed' ? 'success' : 'active'} strokeWidth={10} />
{errorMessage && ( </div>
<Alert <div style={{ flex: 1, overflowY: 'auto' }}>
type="error" <List
message={errorMessage} size="small"
showIcon dataSource={progressData}
style={{ marginBottom: 24, width: '100%' }} 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> </div>
) : (
<>
<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>
</>
)}
</Card> </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> </Col>
</Row> </Row>
+44 -35
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 }}>
{quickActions.map((action, idx) => ( <div style={{ fontSize: 18, marginBottom: 16, fontWeight: 600, color: '#333' }}></div>
<Col span={8} key={idx}> <Row gutter={24}>
<Card {quickActions.map((action, idx) => (
hoverable <Col span={8} key={idx}>
onClick={action.onClick} <Card
styles={{ body: { background: 'linear-gradient(135deg, #f6f8fc 0%, #eef2f9 100%)' } }} hoverable
> onClick={action.onClick}
<Flex align="center" gap={16}> style={{
<Flex borderRadius: 8,
align="center" overflow: 'hidden',
justify="center" border: '1px solid #f0f0f0',
style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
width: 56, }}
height: 56, styles={{ body: { padding: '24px', background: '#fff', height: '100%' } }}
borderRadius: '50%', >
background: '#fff', <Flex align="center" gap={20}>
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.1)', <Flex
color: '#1890ff', align="center"
fontSize: 24 justify="center"
}} style={{
> width: 56,
{action.icon} 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>
<Flex vertical> </Card>
<div style={{ fontWeight: 600, fontSize: 18 }}>{action.title}</div> </Col>
<div style={{ color: 'var(--color-text-secondary)', fontSize: 14 }}>{action.desc}</div> ))}
</Flex> </Row>
</Flex> </div>
</Card>
</Col>
))}
</Row>
<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 {