From 083d12016a58db244e329c6b7fc61c9a8bb7ab4f Mon Sep 17 00:00:00 2001 From: FaulknerWu Date: Sat, 20 Jun 2026 11:20:04 +0800 Subject: [PATCH] Enhance inspection workspace --- .../src/app/inspection/page.tsx | 463 +++++++++++------- customs-tablet-frontend/src/app/page.tsx | 79 +-- .../src/services/mockApi.ts | 35 +- customs-tablet-frontend/src/types/index.ts | 11 + 4 files changed, 374 insertions(+), 214 deletions(-) diff --git a/customs-tablet-frontend/src/app/inspection/page.tsx b/customs-tablet-frontend/src/app/inspection/page.tsx index 339e9a6..f87c70e 100644 --- a/customs-tablet-frontend/src/app/inspection/page.tsx +++ b/customs-tablet-frontend/src/app/inspection/page.tsx @@ -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(null); const [loadingCustoms, setLoadingCustoms] = useState(true); - const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState('idle'); const [logs, setLogs] = useState<{time: string, msg: string, type: 'info'|'warning'|'success'}[]>([]); const [progressData, setProgressData] = useState([]); @@ -52,8 +51,14 @@ function InspectionContent() { const [pauseReason, setPauseReason] = useState(''); const [customsList, setCustomsList] = useState([]); const [loadingList, setLoadingList] = useState(false); - const [currentView, setCurrentView] = useState('摄像头1'); - + + // Issues and cameras + const [issues, setIssues] = useState([]); + const [cameras, setCameras] = useState([]); + + // The segmented control options based on overview cameras + const [currentOverviewCamera, setCurrentOverviewCamera] = useState(''); + const { token } = theme.useToken(); const logsEndRef = useRef(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 = [ + { + title: '时间', + dataIndex: 'time', + key: 'time', + width: 90, + render: (text) => {text} + }, + { + title: '问题描述', + dataIndex: 'description', + key: 'description', + render: (text, record) => ( + + + {text} + + ) + }, + { + 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 {info.text}; + } + }, + { + title: '操作', + key: 'action', + width: 140, + render: (_, record) => ( + record.status === 'pending' ? ( + + + + + ) : null + ), + }, + ]; + if (loadingCustoms) { return ( @@ -212,179 +289,219 @@ function InspectionContent() { return (
- + {/* 顶部工具条:面包屑 + 报关单选择器 + 状态指示灯 */} + + + + {currentCustoms && ( + {status === 'running' ? '作业中' : status === 'idle' ? '待作业' : status === 'paused' ? '已暂停' : '已完成'}} + /> + )} + - (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}`); - }} - /> - - } - styles={{ body: { overflow: 'hidden', padding: '24px', display: 'flex', flexDirection: 'column', flex: 1 } }} - style={{ height: '100%', display: 'flex', flexDirection: 'column', borderRadius: token.borderRadiusLG }} - bordered={false} + {/* 右侧:信息面板 */} + + + {/* 核销进度 */} + 核销进度} + size="small" + style={{ flex: 4, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + styles={{ body: { padding: '12px', flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 } }} > - {!currentCustoms ? ( - - {errorMessage && ( - +
+ +
+
+ ( + +
+ + {item.inventoryName} + + {item.inventoryCode} + + + + +
+
)} - - - ) : ( - <> - - {status === 'idle' || status === 'paused' ? ( - - ) : status === 'running' ? ( - - ) : null} - - - - - 当前核销进度 -
- -
- -
- ( - -
- - {item.inventoryName} - - {item.inventoryCode} - - - - -
-
- )} - /> -
- - 查验日志 -
- {logs.length > 0 ? ( - ({ - color: item.type === 'success' ? 'green' : item.type === 'warning' ? 'orange' : 'blue', - children: ( - - {item.time} - {item.msg} - - ), - }))} - /> - ) : ( - - )} -
-
- - )} + /> +
+ + {/* 查验异常 */} + 查验异常} + size="small" + style={{ flex: 3, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 } }} + > +
+ + + + + {/* 查验日志 */} + 查验日志} + 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 } }} + > +
+ {logs.length > 0 ? ( + ({ + color: item.type === 'success' ? 'green' : item.type === 'warning' ? 'orange' : 'blue', + children: ( + + {item.time} + {item.msg} + + ), + }))} + /> + ) : ( + + )} +
+
+ + diff --git a/customs-tablet-frontend/src/app/page.tsx b/customs-tablet-frontend/src/app/page.tsx index 0356b2e..f07d397 100644 --- a/customs-tablet-frontend/src/app/page.tsx +++ b/customs-tablet-frontend/src/app/page.tsx @@ -100,9 +100,9 @@ export default function DashboardPage() { ]; const quickActions = [ - { title: '扫码查询机器', desc: '使用平板摄像头扫描设备二维码', icon: , onClick: () => router.push('/machines') }, - { title: '序列号查询机器', desc: '手动输入序列号查询机器全部资料', icon: , onClick: () => router.push('/machines') }, - { title: '视频监控', desc: '查看厂房实时监控画面', icon: , onClick: () => router.push('/video') }, + { title: '扫码查询机器', desc: '使用平板摄像头扫描设备二维码', icon: , onClick: () => router.push('/machines'), color: '#1890ff' }, + { title: '序列号查询机器', desc: '手动输入序列号查询机器全部资料', icon: , onClick: () => router.push('/machines'), color: '#1890ff' }, + { title: '视频监控', desc: '查看厂房实时监控画面', icon: , onClick: () => router.push('/video'), color: '#1890ff' }, ]; return ( @@ -135,39 +135,48 @@ export default function DashboardPage() { {/* 快捷操作区域 */} - - {quickActions.map((action, idx) => ( -
- - - - {action.icon} +
+
快捷功能板块
+ + {quickActions.map((action, idx) => ( +
+ + + + {action.icon} + + +
{action.title}
+
{action.desc}
+
- -
{action.title}
-
{action.desc}
-
- -
- - ))} - + + + ))} + + {/* 最近查验动态 */} diff --git a/customs-tablet-frontend/src/services/mockApi.ts b/customs-tablet-frontend/src/services/mockApi.ts index 6914f94..76fe76d 100644 --- a/customs-tablet-frontend/src/services/mockApi.ts +++ b/customs-tablet-frontend/src/services/mockApi.ts @@ -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 => { 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 => { + 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 => { + await delay(200); + return true; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + cancelIssue: async (_id: string): Promise => { + await delay(200); + return true; + }, + getMachineDetail: async (serialNumber: string): Promise => { await delay(400); return { diff --git a/customs-tablet-frontend/src/types/index.ts b/customs-tablet-frontend/src/types/index.ts index dce23ac..69f564e 100644 --- a/customs-tablet-frontend/src/types/index.ts +++ b/customs-tablet-frontend/src/types/index.ts @@ -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 {