Enhance inspection workspace
This commit is contained in:
@@ -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,8 +51,14 @@ 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
|
|
||||||
title={
|
{/* 核销进度 */}
|
||||||
<Flex justify="space-between" align="center">
|
<Card
|
||||||
<span>查验任务</span>
|
title={<Text strong>核销进度</Text>}
|
||||||
<Select
|
size="small"
|
||||||
showSearch
|
style={{ flex: 4, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||||
placeholder="搜索并选择报关单..."
|
styles={{ body: { padding: '12px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 } }}
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user