Enhance inspection workspace
This commit is contained in:
@@ -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,7 +51,13 @@ 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,72 +289,16 @@ function InspectionContent() {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)' }}>
|
||||
{/* 顶部工具条:面包屑 + 报关单选择器 + 状态指示灯 */}
|
||||
<Flex align="center" justify="space-between" style={{ padding: '0 24px', margin: '16px 0' }}>
|
||||
<Breadcrumb />
|
||||
|
||||
<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>
|
||||
)}
|
||||
</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 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>}
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:查验控制面板 */}
|
||||
<Col span={10}>
|
||||
<Card
|
||||
title={
|
||||
<Flex justify="space-between" align="center">
|
||||
<span>查验任务</span>
|
||||
)}
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="搜索并选择报关单..."
|
||||
@@ -297,26 +318,94 @@ function InspectionContent() {
|
||||
}}
|
||||
/>
|
||||
</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}
|
||||
</Flex>
|
||||
|
||||
<Row gutter={24} style={{ flex: 1, minHeight: 0, margin: '0 24px 24px 24px' }}>
|
||||
{/* 左侧:多摄像头 + 控制区 */}
|
||||
<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 ? (
|
||||
<Flex vertical align="center" justify="center" style={{ flex: 1 }}>
|
||||
{errorMessage && (
|
||||
<Alert
|
||||
type="error"
|
||||
message={errorMessage}
|
||||
showIcon
|
||||
style={{ marginBottom: 24, width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
<Empty description="请在右上角选择要查验的报关单" />
|
||||
<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 justify="center" gap="middle" style={{ marginBottom: 32 }}>
|
||||
<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>
|
||||
|
||||
{/* 右列 (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' ? '开始查验' : '继续查验'}
|
||||
@@ -329,23 +418,33 @@ function InspectionContent() {
|
||||
<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>
|
||||
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '0 1 35%', marginBottom: 24, overflowY: 'auto', paddingRight: 8 }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={progressData}
|
||||
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%' }}>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
|
||||
<Text strong>{item.inventoryName}</Text>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 4 }}>
|
||||
<Text strong style={{ fontSize: 13 }}>{item.inventoryName}</Text>
|
||||
<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 }} />
|
||||
</Space>
|
||||
</Flex>
|
||||
@@ -355,16 +454,35 @@ function InspectionContent() {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<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
|
||||
}}>
|
||||
{/* 查验异常 */}
|
||||
<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) => ({
|
||||
@@ -372,7 +490,7 @@ function InspectionContent() {
|
||||
children: (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{item.time}</Text>
|
||||
<Text>{item.msg}</Text>
|
||||
<Text style={{ fontSize: 13 }}>{item.msg}</Text>
|
||||
</Space>
|
||||
),
|
||||
}))}
|
||||
@@ -382,9 +500,8 @@ function InspectionContent() {
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
<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}
|
||||
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
|
||||
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
|
||||
borderRadius: 8,
|
||||
background: '#f0f5ff',
|
||||
color: action.color,
|
||||
fontSize: 28,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
{action.icon}
|
||||
</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 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>
|
||||
</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 {
|
||||
|
||||
Reference in New Issue
Block a user