2026-02-26 11:24:00 +08:00
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Select,
|
2026-03-10 09:20:31 +08:00
|
|
|
|
InputNumber,
|
|
|
|
|
|
DatePicker,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
message,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Statistic,
|
|
|
|
|
|
Progress,
|
|
|
|
|
|
Descriptions,
|
|
|
|
|
|
Empty,
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Tabs,
|
|
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
CloseCircleOutlined,
|
|
|
|
|
|
ExclamationCircleOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
InboxOutlined,
|
|
|
|
|
|
ScanOutlined,
|
|
|
|
|
|
CheckOutlined,
|
|
|
|
|
|
CloseOutlined,
|
2026-03-06 15:41:07 +08:00
|
|
|
|
PlusOutlined,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
} from '@ant-design/icons';
|
2026-03-29 10:06:32 +08:00
|
|
|
|
import api from '../api';
|
2026-02-26 11:24:00 +08:00
|
|
|
|
import dayjs from 'dayjs';
|
2026-03-10 09:20:31 +08:00
|
|
|
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
2026-02-26 11:24:00 +08:00
|
|
|
|
import { designTokens } from '../config/theme';
|
2026-03-12 12:40:12 +08:00
|
|
|
|
import CloseButton from '../components/CloseButton';
|
2026-02-26 11:24:00 +08:00
|
|
|
|
|
|
|
|
|
|
const InventoryTaskExecution = () => {
|
|
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
2026-03-10 09:20:31 +08:00
|
|
|
|
const navigate = useNavigate();
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const [plan, setPlan] = useState(null);
|
|
|
|
|
|
const [tasks, setTasks] = useState([]);
|
|
|
|
|
|
const [currentTask, setCurrentTask] = useState(null);
|
|
|
|
|
|
const [records, setRecords] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [checkModalVisible, setCheckModalVisible] = useState(false);
|
|
|
|
|
|
const [currentRecord, setCurrentRecord] = useState(null);
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState('1');
|
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState(null);
|
|
|
|
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
|
|
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
|
|
|
|
|
const [batchLoading, setBatchLoading] = useState(false);
|
|
|
|
|
|
const [scanInput, setScanInput] = useState('');
|
|
|
|
|
|
const [scanResult, setScanResult] = useState(null);
|
|
|
|
|
|
const [scanModalVisible, setScanModalVisible] = useState(false);
|
|
|
|
|
|
const [recordPagination, setRecordPagination] = useState({ current: 1, pageSize: 20, total: 0 });
|
2026-03-06 15:41:07 +08:00
|
|
|
|
const [quickAddModalVisible, setQuickAddModalVisible] = useState(false);
|
|
|
|
|
|
const [quickAddForm] = Form.useForm();
|
|
|
|
|
|
const [quickAddLoading, setQuickAddLoading] = useState(false);
|
|
|
|
|
|
const [rooms, setRooms] = useState([]);
|
|
|
|
|
|
const [racks, setRacks] = useState([]);
|
2026-03-10 09:20:31 +08:00
|
|
|
|
const [deviceFields, setDeviceFields] = useState(null); // 初始值改为 null,表示未加载
|
2026-03-06 15:41:07 +08:00
|
|
|
|
const [selectedRoomId, setSelectedRoomId] = useState(null);
|
2026-02-26 11:24:00 +08:00
|
|
|
|
|
|
|
|
|
|
const planId = searchParams.get('planId');
|
|
|
|
|
|
|
|
|
|
|
|
const fetchPlan = useCallback(async () => {
|
|
|
|
|
|
if (!planId) return;
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.get(`/inventory/plans/${planId}`);
|
|
|
|
|
|
setPlan(res.data.plan);
|
|
|
|
|
|
setTasks(res.data.tasks || []);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取盘点计划失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [planId]);
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const fetchTaskRecords = async taskId => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.get(`/inventory/tasks/${taskId}`);
|
|
|
|
|
|
setCurrentTask(res.data.task);
|
|
|
|
|
|
setRecords(res.data.records || []);
|
|
|
|
|
|
setRecordPagination(prev => ({ ...prev, total: res.data.records?.length || 0, current: 1 }));
|
|
|
|
|
|
setSelectedRowKeys([]);
|
|
|
|
|
|
setActiveTab('2');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取盘点记录失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const filteredRecords = records.filter(record => {
|
|
|
|
|
|
if (statusFilter && record.status !== statusFilter) return false;
|
|
|
|
|
|
if (searchKeyword) {
|
|
|
|
|
|
const keyword = searchKeyword.toLowerCase();
|
|
|
|
|
|
return (
|
|
|
|
|
|
(record.deviceName && record.deviceName.toLowerCase().includes(keyword)) ||
|
|
|
|
|
|
(record.deviceId && record.deviceId.toLowerCase().includes(keyword)) ||
|
|
|
|
|
|
(record.serialNumber && record.serialNumber.toLowerCase().includes(keyword))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleBatchCheck = async status => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
if (selectedRowKeys.length === 0) {
|
|
|
|
|
|
message.warning('请先选择要盘点的记录');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setBatchLoading(true);
|
|
|
|
|
|
try {
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const promises = selectedRowKeys.map(recordId =>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
api.post(`/inventory/records/${recordId}/check`, {
|
|
|
|
|
|
status,
|
|
|
|
|
|
checkedAt: new Date().toISOString(),
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
message.success(`已批量标记 ${selectedRowKeys.length} 条记录`);
|
|
|
|
|
|
if (currentTask) {
|
|
|
|
|
|
fetchTaskRecords(currentTask.taskId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('批量操作失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setBatchLoading(false);
|
|
|
|
|
|
setSelectedRowKeys([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleQuickCheckAll = async status => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const pendingRecords = filteredRecords.filter(r => r.status === 'pending');
|
|
|
|
|
|
if (pendingRecords.length === 0) {
|
|
|
|
|
|
message.info('没有待盘点的记录');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setBatchLoading(true);
|
|
|
|
|
|
try {
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const promises = pendingRecords.map(record =>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
api.post(`/inventory/records/${record.recordId}/check`, {
|
|
|
|
|
|
status,
|
|
|
|
|
|
checkedAt: new Date().toISOString(),
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
message.success(`已一键标记 ${pendingRecords.length} 条记录`);
|
|
|
|
|
|
if (currentTask) {
|
|
|
|
|
|
fetchTaskRecords(currentTask.taskId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('操作失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setBatchLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleScan = async sn => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
if (!sn || sn.trim() === '') return;
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const trimmedSN = sn.trim().toUpperCase();
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const matchedRecord = records.find(
|
|
|
|
|
|
r => r.serialNumber && r.serialNumber.toUpperCase() === trimmedSN
|
2026-02-26 11:24:00 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!matchedRecord) {
|
|
|
|
|
|
setScanResult({ success: false, message: `未找到序列号为 "${sn}" 的设备`, sn: trimmedSN });
|
|
|
|
|
|
setScanModalVisible(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (matchedRecord.status !== 'pending') {
|
2026-03-27 19:12:16 +08:00
|
|
|
|
setScanResult({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: `设备 "${matchedRecord.deviceName}" 已盘点`,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
record: matchedRecord,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
sn: trimmedSN,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
});
|
|
|
|
|
|
setScanModalVisible(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post(`/inventory/records/${matchedRecord.recordId}/check`, {
|
|
|
|
|
|
status: 'normal',
|
|
|
|
|
|
actualSerialNumber: matchedRecord.serialNumber,
|
|
|
|
|
|
checkedAt: new Date().toISOString(),
|
|
|
|
|
|
});
|
2026-03-27 19:12:16 +08:00
|
|
|
|
setScanResult({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: `设备 "${matchedRecord.deviceName}" 盘点成功!`,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
record: matchedRecord,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
sn: trimmedSN,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
});
|
|
|
|
|
|
setScanModalVisible(true);
|
|
|
|
|
|
if (currentTask) {
|
|
|
|
|
|
fetchTaskRecords(currentTask.taskId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('盘点失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleScanInput = e => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const value = e.target.value;
|
|
|
|
|
|
setScanInput(value);
|
|
|
|
|
|
if (value.includes('\n') || value.includes('\r')) {
|
|
|
|
|
|
const sn = value.replace(/[\r\n]/g, '').trim();
|
|
|
|
|
|
if (sn) {
|
|
|
|
|
|
handleScan(sn);
|
|
|
|
|
|
setScanInput('');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleScanKeyPress = e => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const sn = e.target.value.trim();
|
|
|
|
|
|
if (sn) {
|
|
|
|
|
|
handleScan(sn);
|
|
|
|
|
|
setScanInput('');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleScanClick = () => {
|
|
|
|
|
|
const sn = scanInput.trim();
|
|
|
|
|
|
if (sn) {
|
|
|
|
|
|
handleScan(sn);
|
|
|
|
|
|
setScanInput('');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.warning('请输入序列号');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchPlan();
|
|
|
|
|
|
}, [fetchPlan]);
|
|
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
const defaultDeviceFields = [
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'deviceName',
|
|
|
|
|
|
displayName: '设备名称',
|
|
|
|
|
|
fieldType: 'string',
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'deviceType',
|
|
|
|
|
|
displayName: '设备类型',
|
|
|
|
|
|
fieldType: 'select',
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 2,
|
|
|
|
|
|
options: [
|
|
|
|
|
|
{ value: 'server', label: '服务器' },
|
|
|
|
|
|
{ value: 'switch', label: '交换机' },
|
|
|
|
|
|
{ value: 'router', label: '路由器' },
|
|
|
|
|
|
{ value: 'storage', label: '存储设备' },
|
|
|
|
|
|
{ value: 'other', label: '其他' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'model',
|
|
|
|
|
|
displayName: '型号',
|
|
|
|
|
|
fieldType: 'string',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 3,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'serialNumber',
|
|
|
|
|
|
displayName: '序列号',
|
|
|
|
|
|
fieldType: 'string',
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 4,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'roomId',
|
|
|
|
|
|
displayName: '所属机房',
|
|
|
|
|
|
fieldType: 'select',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 5,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'rackId',
|
|
|
|
|
|
displayName: '所属机柜',
|
|
|
|
|
|
fieldType: 'select',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 6,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'position',
|
|
|
|
|
|
displayName: '位置(U)',
|
|
|
|
|
|
fieldType: 'number',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 7,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'height',
|
|
|
|
|
|
displayName: '高度(U)',
|
|
|
|
|
|
fieldType: 'number',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 8,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'powerConsumption',
|
|
|
|
|
|
displayName: '功率(W)',
|
|
|
|
|
|
fieldType: 'number',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 9,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'purchaseDate',
|
|
|
|
|
|
displayName: '购买日期',
|
|
|
|
|
|
fieldType: 'date',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 10,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'warrantyExpiry',
|
|
|
|
|
|
displayName: '保修到期',
|
|
|
|
|
|
fieldType: 'date',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 11,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'ipAddress',
|
|
|
|
|
|
displayName: 'IP地址',
|
|
|
|
|
|
fieldType: 'string',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 12,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'brand',
|
|
|
|
|
|
displayName: '品牌',
|
|
|
|
|
|
fieldType: 'string',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 13,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'description',
|
|
|
|
|
|
displayName: '描述',
|
|
|
|
|
|
fieldType: 'textarea',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 14,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
fieldName: 'remark',
|
|
|
|
|
|
displayName: '备注',
|
|
|
|
|
|
fieldType: 'textarea',
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
order: 15,
|
|
|
|
|
|
},
|
2026-03-10 09:20:31 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const fetchDeviceFields = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.get('/deviceFields');
|
|
|
|
|
|
const sortedFields = res.data.sort((a, b) => a.order - b.order);
|
|
|
|
|
|
setDeviceFields(sortedFields);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取字段配置失败:', error);
|
|
|
|
|
|
setDeviceFields(defaultDeviceFields);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-06 15:41:07 +08:00
|
|
|
|
const fetchRooms = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.get('/rooms');
|
|
|
|
|
|
setRooms(res.data.rooms || res.data || []);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取机房列表失败', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchRacks = async () => {
|
|
|
|
|
|
try {
|
2026-03-10 09:20:31 +08:00
|
|
|
|
const res = await api.get('/racks', { params: { pageSize: 1000 } });
|
2026-03-06 15:41:07 +08:00
|
|
|
|
setRacks(res.data.racks || res.data || []);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取机柜列表失败', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchRooms();
|
|
|
|
|
|
fetchRacks();
|
2026-03-10 09:20:31 +08:00
|
|
|
|
fetchDeviceFields();
|
2026-03-06 15:41:07 +08:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const filteredRacks = selectedRoomId
|
2026-03-06 15:41:07 +08:00
|
|
|
|
? racks.filter(rack => rack.roomId === selectedRoomId)
|
|
|
|
|
|
: racks;
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const renderQuickAddFormField = field => {
|
2026-03-10 09:20:31 +08:00
|
|
|
|
const { fieldName, displayName, fieldType, required, options } = field;
|
|
|
|
|
|
|
|
|
|
|
|
// 统一的表单项样式
|
|
|
|
|
|
const formItemStyle = {
|
|
|
|
|
|
marginBottom: 16,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 统一的输入框样式
|
|
|
|
|
|
const inputStyle = {
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldName === 'roomId' || fieldName === 'rackId') {
|
|
|
|
|
|
// 机房和机柜字段已在Modal中单独处理
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldName === 'deviceType') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
key={fieldName}
|
|
|
|
|
|
name={fieldName}
|
|
|
|
|
|
label={displayName}
|
|
|
|
|
|
initialValue="other"
|
|
|
|
|
|
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
|
|
|
|
|
style={formItemStyle}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Select placeholder={`请选择${displayName}`} size="large" style={inputStyle}>
|
|
|
|
|
|
{(Array.isArray(options) && options.length > 0
|
|
|
|
|
|
? options
|
|
|
|
|
|
: [
|
|
|
|
|
|
{ value: 'server', label: '服务器' },
|
|
|
|
|
|
{ value: 'switch', label: '交换机' },
|
|
|
|
|
|
{ value: 'router', label: '路由器' },
|
|
|
|
|
|
{ value: 'storage', label: '存储设备' },
|
|
|
|
|
|
{ value: 'other', label: '其他' },
|
|
|
|
|
|
]
|
|
|
|
|
|
).map(opt => (
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<Select.Option key={opt.value} value={opt.value}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
filter: 'grayscale(0.3)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{opt.value === 'server' && '🖥️'}
|
|
|
|
|
|
{opt.value === 'switch' && '🔀'}
|
|
|
|
|
|
{opt.value === 'router' && '📡'}
|
|
|
|
|
|
{opt.value === 'storage' && '💾'}
|
|
|
|
|
|
{opt.value === 'other' && '📦'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span>{opt.label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldType === 'select') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
key={fieldName}
|
|
|
|
|
|
name={fieldName}
|
|
|
|
|
|
label={displayName}
|
|
|
|
|
|
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
|
|
|
|
|
style={formItemStyle}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Select placeholder={`请选择${displayName}`} allowClear size="large" style={inputStyle}>
|
2026-03-16 17:07:20 +08:00
|
|
|
|
{(Array.isArray(options) ? options : []).map(opt => (
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<Select.Option key={opt.value} value={opt.value}>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldType === 'number') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
key={fieldName}
|
|
|
|
|
|
name={fieldName}
|
|
|
|
|
|
label={displayName}
|
|
|
|
|
|
rules={required ? [{ required: true, message: `请输入${displayName}` }] : []}
|
|
|
|
|
|
style={formItemStyle}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
placeholder={`请输入${displayName}`}
|
2026-03-10 09:20:31 +08:00
|
|
|
|
min={1}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldType === 'date') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
key={fieldName}
|
|
|
|
|
|
name={fieldName}
|
|
|
|
|
|
label={displayName}
|
|
|
|
|
|
rules={required ? [{ required: true, message: `请选择${displayName}` }] : []}
|
|
|
|
|
|
style={formItemStyle}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<DatePicker
|
2026-03-10 09:20:31 +08:00
|
|
|
|
style={{ ...inputStyle, width: '100%' }}
|
|
|
|
|
|
placeholder={`请选择${displayName}`}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldType === 'text' || fieldType === 'textarea') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
key={fieldName}
|
|
|
|
|
|
name={fieldName}
|
|
|
|
|
|
label={displayName}
|
|
|
|
|
|
rules={required ? [{ required: true, message: `请输入${displayName}` }] : []}
|
|
|
|
|
|
style={formItemStyle}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Input.TextArea
|
|
|
|
|
|
rows={3}
|
2026-03-10 09:20:31 +08:00
|
|
|
|
placeholder={`请输入${displayName}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...inputStyle,
|
|
|
|
|
|
resize: 'none',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
key={fieldName}
|
|
|
|
|
|
name={fieldName}
|
|
|
|
|
|
label={displayName}
|
|
|
|
|
|
rules={required ? [{ required: true, message: `请输入${displayName}` }] : []}
|
|
|
|
|
|
style={formItemStyle}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Input placeholder={`请输入${displayName}`} size="large" style={inputStyle} />
|
2026-03-10 09:20:31 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderQuickAddFormFields = () => {
|
|
|
|
|
|
// 如果字段配置未加载,使用默认配置
|
|
|
|
|
|
const fields = deviceFields || defaultDeviceFields;
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
// 排除机房和机柜字段(已在Modal中单独渲染)
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const visibleFields = fields.filter(
|
|
|
|
|
|
f =>
|
|
|
|
|
|
f.visible !== false &&
|
|
|
|
|
|
f.fieldName !== 'serialNumber' &&
|
|
|
|
|
|
f.fieldName !== 'roomId' &&
|
|
|
|
|
|
f.fieldName !== 'rackId'
|
2026-03-10 09:20:31 +08:00
|
|
|
|
);
|
|
|
|
|
|
const sortedFields = visibleFields.sort((a, b) => a.order - b.order);
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
// 将字段分成两列
|
|
|
|
|
|
const leftFields = [];
|
|
|
|
|
|
const rightFields = [];
|
|
|
|
|
|
const fullFields = [];
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
sortedFields.forEach((field, index) => {
|
|
|
|
|
|
// 文本域类型独占一行
|
|
|
|
|
|
if (field.fieldType === 'text') {
|
|
|
|
|
|
fullFields.push(field);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他字段交替排列
|
|
|
|
|
|
if (index % 2 === 0) {
|
|
|
|
|
|
leftFields.push(field);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
rightFields.push(field);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* 两列布局的字段 */}
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col xs={24} sm={12}>
|
|
|
|
|
|
{leftFields.map(renderQuickAddFormField)}
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={12}>
|
|
|
|
|
|
{rightFields.map(renderQuickAddFormField)}
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{/* 独占一行的字段(如备注) */}
|
|
|
|
|
|
{fullFields.map(renderQuickAddFormField)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleQuickAddDevice = async values => {
|
2026-03-10 09:20:31 +08:00
|
|
|
|
if (!plan) {
|
|
|
|
|
|
message.error('盘点计划不存在');
|
2026-03-06 15:41:07 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setQuickAddLoading(true);
|
|
|
|
|
|
try {
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const { purchaseDate, warrantyExpiry, ...otherValues } = values;
|
|
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
const payload = {
|
|
|
|
|
|
taskId: currentTask?.taskId || null,
|
2026-03-06 15:41:07 +08:00
|
|
|
|
planId: plan.planId,
|
|
|
|
|
|
serialNumber: scanResult?.sn || values.serialNumber,
|
2026-03-10 09:20:31 +08:00
|
|
|
|
...otherValues,
|
|
|
|
|
|
purchaseDate: purchaseDate ? dayjs(purchaseDate).toISOString() : null,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
warrantyExpiry: warrantyExpiry ? dayjs(warrantyExpiry).toISOString() : null,
|
2026-03-10 09:20:31 +08:00
|
|
|
|
};
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
const res = await api.post('/inventory/quick-add-device', payload);
|
|
|
|
|
|
message.success('设备已暂存!');
|
2026-03-06 15:41:07 +08:00
|
|
|
|
setQuickAddModalVisible(false);
|
2026-03-10 09:20:31 +08:00
|
|
|
|
setSelectedRoomId(null);
|
2026-03-06 15:41:07 +08:00
|
|
|
|
quickAddForm.resetFields();
|
|
|
|
|
|
setScanResult({
|
|
|
|
|
|
success: true,
|
2026-03-10 09:20:31 +08:00
|
|
|
|
message: `设备 "${res.data.pendingDevice.deviceName}" 已暂存,请前往「暂存设备」页面完善信息后同步到设备管理`,
|
|
|
|
|
|
pendingDevice: res.data.pendingDevice,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
sn: res.data.pendingDevice.serialNumber,
|
2026-03-06 15:41:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
2026-03-10 09:20:31 +08:00
|
|
|
|
message.error(error.response?.data?.error || '暂存设备失败');
|
2026-03-06 15:41:07 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setQuickAddLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openQuickAddModal = () => {
|
|
|
|
|
|
setSelectedRoomId(null);
|
|
|
|
|
|
quickAddForm.setFieldsValue({
|
|
|
|
|
|
serialNumber: scanResult?.sn || '',
|
|
|
|
|
|
deviceName: '',
|
|
|
|
|
|
deviceType: 'other',
|
|
|
|
|
|
roomId: undefined,
|
|
|
|
|
|
rackId: undefined,
|
|
|
|
|
|
position: undefined,
|
|
|
|
|
|
remark: undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
setQuickAddModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleCheck = record => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
setCurrentRecord(record);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
actualSerialNumber: record.serialNumber,
|
|
|
|
|
|
actualRackId: record.rackId,
|
|
|
|
|
|
actualPosition: record.position,
|
|
|
|
|
|
status: record.status === 'pending' ? null : record.status,
|
|
|
|
|
|
remark: record.remark,
|
|
|
|
|
|
});
|
|
|
|
|
|
setCheckModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleQuickCheck = async (record, status) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post(`/inventory/records/${record.recordId}/check`, {
|
|
|
|
|
|
status,
|
|
|
|
|
|
checkedAt: new Date().toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
message.success(status === 'normal' ? '标记正常' : '标记异常');
|
|
|
|
|
|
if (currentTask) {
|
|
|
|
|
|
fetchTaskRecords(currentTask.taskId);
|
|
|
|
|
|
}
|
|
|
|
|
|
fetchPlan();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error(error.response?.data?.error || '标记失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleSubmitCheck = async values => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
if (!currentRecord) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post(`/inventory/records/${currentRecord.recordId}/check`, {
|
|
|
|
|
|
actualSerialNumber: values.actualSerialNumber,
|
|
|
|
|
|
actualRackId: values.actualRackId,
|
|
|
|
|
|
actualPosition: values.actualPosition,
|
|
|
|
|
|
status: values.status,
|
|
|
|
|
|
remark: values.remark,
|
|
|
|
|
|
});
|
|
|
|
|
|
message.success('盘点提交成功');
|
|
|
|
|
|
setCheckModalVisible(false);
|
|
|
|
|
|
if (currentTask) {
|
|
|
|
|
|
fetchTaskRecords(currentTask.taskId);
|
|
|
|
|
|
}
|
|
|
|
|
|
fetchPlan();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error(error.response?.data?.error || '提交失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const getStatusTag = status => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const statusMap = {
|
|
|
|
|
|
pending: { color: 'default', text: '待盘点', icon: <ExclamationCircleOutlined /> },
|
|
|
|
|
|
normal: { color: 'success', text: '正常', icon: <CheckCircleOutlined /> },
|
|
|
|
|
|
abnormal: { color: 'error', text: '异常', icon: <CloseCircleOutlined /> },
|
|
|
|
|
|
missed: { color: 'warning', text: '漏盘', icon: <ExclamationCircleOutlined /> },
|
|
|
|
|
|
};
|
|
|
|
|
|
const config = statusMap[status] || statusMap.pending;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Tag color={config.color} icon={config.icon}>
|
|
|
|
|
|
{config.text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const getAbnormalTypeTag = type => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const typeMap = {
|
|
|
|
|
|
serial_mismatch: '序列号不符',
|
|
|
|
|
|
position_mismatch: '位置不符',
|
|
|
|
|
|
device_missing: '设备缺失',
|
|
|
|
|
|
extra_device: '多出设备',
|
|
|
|
|
|
};
|
|
|
|
|
|
return typeMap[type] || type;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const taskColumns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '任务信息',
|
|
|
|
|
|
key: 'taskInfo',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontWeight: 600 }}>{record.targetName}</div>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: designTokens.colors.text.tertiary }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{record.targetType === 'room'
|
|
|
|
|
|
? '机房'
|
|
|
|
|
|
: record.targetType === 'rack'
|
|
|
|
|
|
? '机柜'
|
|
|
|
|
|
: '全部设备'}
|
2026-02-26 11:24:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
width: 100,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
render: status => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const statusMap = {
|
|
|
|
|
|
pending: <Badge status="default" text="待执行" />,
|
|
|
|
|
|
in_progress: <Badge status="processing" text="进行中" />,
|
|
|
|
|
|
completed: <Badge status="success" text="已完成" />,
|
|
|
|
|
|
skipped: <Badge status="warning" text="已跳过" />,
|
|
|
|
|
|
};
|
|
|
|
|
|
return statusMap[status] || status;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '设备进度',
|
|
|
|
|
|
key: 'progress',
|
|
|
|
|
|
width: 150,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Progress
|
2026-03-27 19:12:16 +08:00
|
|
|
|
percent={
|
|
|
|
|
|
record.totalDevices > 0
|
|
|
|
|
|
? Math.round((record.checkedDevices / record.totalDevices) * 100)
|
|
|
|
|
|
: 0
|
|
|
|
|
|
}
|
2026-02-26 11:24:00 +08:00
|
|
|
|
size="small"
|
|
|
|
|
|
format={() => `${record.checkedDevices}/${record.totalDevices}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '异常',
|
|
|
|
|
|
dataIndex: 'abnormalDevices',
|
|
|
|
|
|
key: 'abnormalDevices',
|
|
|
|
|
|
width: 80,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
render: count => (count > 0 ? <Tag color="error">{count}</Tag> : '-'),
|
2026-02-26 11:24:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setCurrentTask(record);
|
|
|
|
|
|
fetchTaskRecords(record.taskId);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
查看详情
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const recordColumns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '设备信息',
|
|
|
|
|
|
key: 'deviceInfo',
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontWeight: 600 }}>{record.deviceName}</div>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: designTokens.colors.text.tertiary }}>
|
|
|
|
|
|
{record.deviceId}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '类型',
|
|
|
|
|
|
dataIndex: 'deviceType',
|
|
|
|
|
|
key: 'deviceType',
|
|
|
|
|
|
width: 80,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
render: type => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
const typeMap = {
|
|
|
|
|
|
server: '服务器',
|
|
|
|
|
|
switch: '交换机',
|
|
|
|
|
|
router: '路由器',
|
|
|
|
|
|
storage: '存储设备',
|
|
|
|
|
|
other: '其他',
|
|
|
|
|
|
};
|
|
|
|
|
|
return typeMap[type] || type;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '系统序列号',
|
|
|
|
|
|
dataIndex: 'serialNumber',
|
|
|
|
|
|
key: 'serialNumber',
|
|
|
|
|
|
width: 140,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
render: sn => <code style={{ fontSize: '12px' }}>{sn}</code>,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '实际序列号',
|
|
|
|
|
|
dataIndex: 'actualSerialNumber',
|
|
|
|
|
|
key: 'actualSerialNumber',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (sn, record) => (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{sn ? (
|
|
|
|
|
|
<code style={{ fontSize: '12px' }}>{sn}</code>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span style={{ color: designTokens.colors.text.tertiary }}>-</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{record.abnormalType === 'serial_mismatch' && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Tag color="error" style={{ marginLeft: 4 }}>
|
|
|
|
|
|
不符
|
|
|
|
|
|
</Tag>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '系统位置',
|
|
|
|
|
|
dataIndex: 'displayLocation',
|
|
|
|
|
|
key: 'displayLocation',
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<span>{record.displayLocation || `${record.rackId} - U${record.position}`}</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '实际位置',
|
|
|
|
|
|
key: 'actualPosition',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{record.actualRackId ? (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
{record.actualRackId} - U{record.actualPosition}
|
|
|
|
|
|
</div>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<span style={{ color: designTokens.colors.text.tertiary }}>-</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{record.abnormalType === 'position_mismatch' && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Tag color="error" style={{ marginLeft: 4 }}>
|
|
|
|
|
|
不符
|
|
|
|
|
|
</Tag>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
width: 90,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
render: status => getStatusTag(status),
|
2026-02-26 11:24:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '盘点人',
|
|
|
|
|
|
dataIndex: ['Checker', 'realName'],
|
|
|
|
|
|
key: 'checker',
|
|
|
|
|
|
width: 90,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
render: name => name || '-',
|
2026-02-26 11:24:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '盘点时间',
|
|
|
|
|
|
dataIndex: 'checkedAt',
|
|
|
|
|
|
key: 'checkedAt',
|
|
|
|
|
|
width: 140,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
render: date => (date ? dayjs(date).format('MM-DD HH:mm') : '-'),
|
2026-02-26 11:24:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
{record.status === 'pending' ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Tooltip title="标记正常">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<CheckCircleOutlined />}
|
|
|
|
|
|
onClick={() => handleQuickCheck(record, 'normal')}
|
|
|
|
|
|
style={{ color: '#52c41a' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="标记异常">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<CloseCircleOutlined />}
|
|
|
|
|
|
onClick={() => handleQuickCheck(record, 'abnormal')}
|
|
|
|
|
|
style={{ color: '#ff4d4f' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : null}
|
2026-03-25 09:16:57 +08:00
|
|
|
|
<Button type="link" onClick={() => handleCheck(record)}>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
盘点
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const pageContainerStyle = {
|
|
|
|
|
|
padding: '24px',
|
2026-03-16 17:07:20 +08:00
|
|
|
|
background: designTokens.colors.background.secondary,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!planId) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={pageContainerStyle}>
|
|
|
|
|
|
<Card bordered={false}>
|
|
|
|
|
|
<Empty description="请选择要执行的盘点计划" />
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={pageContainerStyle}>
|
|
|
|
|
|
<Card bordered={false} style={{ marginBottom: 16, borderRadius: 12 }}>
|
|
|
|
|
|
<Row gutter={16} align="middle">
|
|
|
|
|
|
<Col flex="auto">
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
|
|
|
|
<InboxOutlined style={{ fontSize: 32, color: designTokens.colors.primary.main }} />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontSize: 18, fontWeight: 600 }}>{plan?.name || '加载中...'}</div>
|
|
|
|
|
|
<div style={{ fontSize: 12, color: designTokens.colors.text.secondary }}>
|
|
|
|
|
|
计划ID: {planId}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col>
|
|
|
|
|
|
<Space>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Statistic title="总设备" value={plan?.totalDevices || 0} />
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="已盘点"
|
|
|
|
|
|
value={plan?.checkedDevices || 0}
|
|
|
|
|
|
valueStyle={{ color: '#52c41a' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title="正常"
|
|
|
|
|
|
value={plan?.normalDevices || 0}
|
|
|
|
|
|
valueStyle={{ color: '#52c41a' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title="异常"
|
|
|
|
|
|
value={plan?.abnormalDevices || 0}
|
|
|
|
|
|
valueStyle={{ color: '#ff4d4f' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title="漏盘"
|
|
|
|
|
|
value={plan?.missedDevices || 0}
|
|
|
|
|
|
valueStyle={{ color: '#faad14' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
{plan?.totalDevices > 0 && (
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={Math.round((plan.checkedDevices / plan.totalDevices) * 100)}
|
|
|
|
|
|
status={plan.abnormalDevices > 0 ? 'exception' : 'active'}
|
|
|
|
|
|
style={{ marginTop: 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card bordered={false} style={{ borderRadius: 12 }}>
|
|
|
|
|
|
<Tabs
|
|
|
|
|
|
activeKey={activeTab}
|
|
|
|
|
|
onChange={setActiveTab}
|
|
|
|
|
|
items={[
|
|
|
|
|
|
{
|
|
|
|
|
|
key: '1',
|
|
|
|
|
|
label: '盘点任务',
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={taskColumns}
|
|
|
|
|
|
dataSource={tasks}
|
|
|
|
|
|
rowKey="taskId"
|
|
|
|
|
|
pagination={false}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: '2',
|
|
|
|
|
|
label: (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
盘点记录
|
|
|
|
|
|
{records.length > 0 && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Tag
|
|
|
|
|
|
style={{ marginLeft: 8 }}
|
|
|
|
|
|
color={plan?.status === 'completed' ? 'success' : 'processing'}
|
|
|
|
|
|
>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
{filteredRecords.length}/{records.length}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: 16,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
gap: 12,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}
|
|
|
|
|
|
>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="筛选状态"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
style={{ width: 120, height: 32 }}
|
|
|
|
|
|
className="统一输入框"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
value={statusFilter}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onChange={value => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
setStatusFilter(value);
|
|
|
|
|
|
setRecordPagination(prev => ({ ...prev, current: 1 }));
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select.Option value="pending">待盘点</Select.Option>
|
|
|
|
|
|
<Select.Option value="normal">正常</Select.Option>
|
|
|
|
|
|
<Select.Option value="abnormal">异常</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索设备名称/ID/序列号"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
|
|
|
|
|
|
style={{ width: 250, height: 32 }}
|
|
|
|
|
|
className="统一输入框"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
value={searchKeyword}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onChange={e => {
|
2026-02-26 11:24:00 +08:00
|
|
|
|
setSearchKeyword(e.target.value);
|
|
|
|
|
|
setRecordPagination(prev => ({ ...prev, current: 1 }));
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
2026-02-26 11:24:00 +08:00
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => setScanModalVisible(true)}
|
|
|
|
|
|
icon={<ScanOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
扫码盘点
|
|
|
|
|
|
</Button>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
2026-02-26 11:24:00 +08:00
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => handleQuickCheckAll('normal')}
|
|
|
|
|
|
loading={batchLoading}
|
|
|
|
|
|
disabled={filteredRecords.filter(r => r.status === 'pending').length === 0}
|
|
|
|
|
|
icon={<CheckCircleOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
一键正常
|
|
|
|
|
|
</Button>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
2026-02-26 11:24:00 +08:00
|
|
|
|
danger
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => handleQuickCheckAll('abnormal')}
|
|
|
|
|
|
loading={batchLoading}
|
|
|
|
|
|
disabled={filteredRecords.filter(r => r.status === 'pending').length === 0}
|
|
|
|
|
|
icon={<CloseCircleOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
一键异常
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-02-26 11:24:00 +08:00
|
|
|
|
{records.length > 0 && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Progress
|
|
|
|
|
|
percent={Math.round(
|
|
|
|
|
|
(records.filter(r => r.status !== 'pending').length / records.length) * 100
|
|
|
|
|
|
)}
|
2026-02-26 11:24:00 +08:00
|
|
|
|
status="active"
|
|
|
|
|
|
strokeColor={{ from: '#108ee9', to: '#87d068' }}
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Table
|
|
|
|
|
|
rowSelection={{
|
|
|
|
|
|
selectedRowKeys,
|
|
|
|
|
|
onChange: setSelectedRowKeys,
|
|
|
|
|
|
}}
|
|
|
|
|
|
columns={recordColumns}
|
|
|
|
|
|
dataSource={filteredRecords}
|
|
|
|
|
|
rowKey="recordId"
|
2026-03-27 19:12:16 +08:00
|
|
|
|
pagination={{
|
|
|
|
|
|
current: recordPagination.current,
|
|
|
|
|
|
pageSize: recordPagination.pageSize,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
total: recordPagination.total,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showTotal: total => `共 ${total} 条`,
|
2026-02-26 11:24:00 +08:00
|
|
|
|
onChange: (page, pageSize) => {
|
|
|
|
|
|
setRecordPagination(prev => ({ ...prev, current: page, pageSize }));
|
2026-03-27 19:12:16 +08:00
|
|
|
|
},
|
2026-02-26 11:24:00 +08:00
|
|
|
|
}}
|
|
|
|
|
|
scroll={{ x: 1400 }}
|
|
|
|
|
|
loading={batchLoading}
|
|
|
|
|
|
locale={{
|
2026-03-27 19:12:16 +08:00
|
|
|
|
emptyText: (
|
|
|
|
|
|
<Empty description="暂无盘点记录" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
|
|
|
),
|
2026-02-26 11:24:00 +08:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title={null}
|
|
|
|
|
|
open={scanModalVisible}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setScanModalVisible(false);
|
|
|
|
|
|
setScanInput('');
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={560}
|
|
|
|
|
|
centered
|
|
|
|
|
|
closeIcon={null}
|
|
|
|
|
|
styles={{
|
|
|
|
|
|
header: { display: 'none' },
|
|
|
|
|
|
body: { padding: 0 },
|
2026-03-27 19:12:16 +08:00
|
|
|
|
content: { borderRadius: 16, overflow: 'hidden' },
|
2026-02-26 11:24:00 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 自定义 Header */}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
padding: '16px 24px',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #69c0ff 0%, #b37feb 100%)',
|
|
|
|
|
|
color: '#1f1f1f',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
background: 'rgba(255,255,255,0.3)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<ScanOutlined style={{ fontSize: 20, color: '#fff' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span style={{ fontSize: 18, fontWeight: 600, color: '#fff' }}>扫码盘点</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<CloseOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setScanModalVisible(false);
|
|
|
|
|
|
setScanInput('');
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
justifyContent: 'center',
|
2026-02-26 11:24:00 +08:00
|
|
|
|
}}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(0,0,0,0.1)')}
|
|
|
|
|
|
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
2026-02-26 11:24:00 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ padding: 24 }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)',
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
padding: 24,
|
|
|
|
|
|
marginBottom: 20,
|
|
|
|
|
|
border: '1px solid #91d5ff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<div style={{ textAlign: 'center', marginBottom: 16 }}>
|
|
|
|
|
|
<ScanOutlined style={{ fontSize: 48, color: '#1890ff' }} />
|
|
|
|
|
|
</div>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="请用扫码枪扫描或输入设备序列号..."
|
|
|
|
|
|
value={scanInput}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onChange={e => setScanInput(e.target.value)}
|
2026-03-10 09:20:31 +08:00
|
|
|
|
onKeyDown={handleScanKeyPress}
|
|
|
|
|
|
variant="outlined"
|
2026-03-27 19:12:16 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
textAlign: 'center',
|
2026-03-10 09:20:31 +08:00
|
|
|
|
height: 40,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.1)',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
width: 400,
|
2026-03-10 09:20:31 +08:00
|
|
|
|
}}
|
|
|
|
|
|
suffix={
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
2026-03-10 09:20:31 +08:00
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={handleScanClick}
|
|
|
|
|
|
style={{ height: 32, borderRadius: 6 }}
|
|
|
|
|
|
icon={<CheckOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
盘点
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
}
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12, textAlign: 'center' }}>
|
|
|
|
|
|
扫码枪扫描或手动输入序列号后按回车 / 点击盘点
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{scanResult && (
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
style={{
|
2026-02-26 11:24:00 +08:00
|
|
|
|
marginBottom: 16,
|
2026-03-10 09:20:31 +08:00
|
|
|
|
borderRadius: designTokens.borderRadius.large,
|
|
|
|
|
|
border: `2px solid ${scanResult.success ? designTokens.colors.success.main : designTokens.colors.error.main}`,
|
|
|
|
|
|
boxShadow: designTokens.shadows.large,
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
}}
|
|
|
|
|
|
bodyStyle={{ padding: 0 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 顶部状态条 */}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: scanResult.success
|
|
|
|
|
|
? designTokens.colors.success.gradient
|
|
|
|
|
|
: designTokens.colors.error.gradient,
|
|
|
|
|
|
padding: '20px 24px',
|
2026-03-10 09:20:31 +08:00
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
gap: 16,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 56,
|
|
|
|
|
|
height: 56,
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: 'rgba(255, 255, 255, 0.25)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
backdropFilter: 'blur(10px)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.success ? (
|
|
|
|
|
|
<CheckOutlined style={{ fontSize: 28, color: '#fff' }} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<CloseOutlined style={{ fontSize: 28, color: '#fff' }} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: 1 }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xl,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.success ? '盘点成功' : '未找到设备'}
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.base,
|
|
|
|
|
|
color: 'rgba(255, 255, 255, 0.9)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 设备信息卡片 */}
|
|
|
|
|
|
{scanResult.record && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '20px 24px',
|
|
|
|
|
|
background: designTokens.colors.background.secondary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.text.secondary,
|
|
|
|
|
|
marginBottom: 12,
|
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
|
letterSpacing: '0.5px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
设备信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Row gutter={[16, 12]}>
|
|
|
|
|
|
<Col xs={24} sm={12} md={8}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
设备ID
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.base,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.record.deviceId}
|
|
|
|
|
|
</div>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<Col xs={24} sm={12} md={8}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
设备名称
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.base,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.record.deviceName}
|
|
|
|
|
|
</div>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
</div>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={12} md={8}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
序列号
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontFamily: 'Consolas, Monaco, monospace',
|
|
|
|
|
|
color: designTokens.colors.primary.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.record.serialNumber}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={24} md={24}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
当前位置
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.base,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<Tag color="blue" style={{ marginRight: 8 }}>
|
|
|
|
|
|
{scanResult.record.displayLocation || `${scanResult.record.rackId}`}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
<Tag color="geekblue">U{scanResult.record.position}</Tag>
|
|
|
|
|
|
</div>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-10 09:20:31 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 暂存设备信息 */}
|
|
|
|
|
|
{scanResult.pendingDevice && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '20px 24px',
|
|
|
|
|
|
background: designTokens.colors.background.secondary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.text.secondary,
|
|
|
|
|
|
marginBottom: 12,
|
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
|
letterSpacing: '0.5px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
暂存设备信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Row gutter={[16, 12]}>
|
|
|
|
|
|
<Col xs={24} sm={12} md={8}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
暂存ID
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.base,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.pendingDevice.pendingId}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={12} md={8}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
设备名称
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.base,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.pendingDevice.deviceName}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={12} md={8}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
序列号
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontFamily: 'Consolas, Monaco, monospace',
|
|
|
|
|
|
color: designTokens.colors.primary.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.pendingDevice.serialNumber}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={12} md={12}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.xs,
|
|
|
|
|
|
color: designTokens.colors.text.tertiary,
|
|
|
|
|
|
marginBottom: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
设备类型
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.base,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color={
|
|
|
|
|
|
designTokens.colors.device[scanResult.pendingDevice.deviceType] ||
|
|
|
|
|
|
'default'
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{scanResult.pendingDevice.deviceType}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
2026-03-10 09:20:31 +08:00
|
|
|
|
type="primary"
|
|
|
|
|
|
onClick={() => navigate('/pending-devices')}
|
|
|
|
|
|
icon={<InboxOutlined />}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
paddingLeft: 24,
|
|
|
|
|
|
paddingRight: 24,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
前往暂存设备页面完善信息
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 快速添加按钮 */}
|
2026-03-06 15:41:07 +08:00
|
|
|
|
{!scanResult.success && scanResult.sn && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '20px 24px',
|
|
|
|
|
|
background: designTokens.colors.background.tertiary,
|
|
|
|
|
|
borderTop: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-06 15:41:07 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
2026-03-10 09:20:31 +08:00
|
|
|
|
size="large"
|
2026-03-06 15:41:07 +08:00
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
|
onClick={openQuickAddModal}
|
2026-03-10 09:20:31 +08:00
|
|
|
|
block
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
fontSize: designTokens.typography.md,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
}}
|
2026-03-06 15:41:07 +08:00
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
快速添加此设备到系统
|
2026-03-06 15:41:07 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-10 09:20:31 +08:00
|
|
|
|
</Card>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{scanResult && (
|
|
|
|
|
|
<div style={{ display: 'flex', gap: 12, justifyContent: 'center' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
2026-02-26 11:24:00 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setScanResult(null);
|
|
|
|
|
|
setScanInput('');
|
|
|
|
|
|
}}
|
|
|
|
|
|
icon={<ScanOutlined />}
|
|
|
|
|
|
style={{ borderRadius: 8 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
继续扫描
|
|
|
|
|
|
</Button>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
2026-02-26 11:24:00 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setScanModalVisible(false);
|
|
|
|
|
|
setScanInput('');
|
|
|
|
|
|
setScanResult(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ borderRadius: 8 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
完成盘点
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!scanResult && (
|
|
|
|
|
|
<div style={{ textAlign: 'center', marginTop: 24 }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="今日已盘点"
|
|
|
|
|
|
value={records.filter(r => r.status !== 'pending').length}
|
2026-02-26 11:24:00 +08:00
|
|
|
|
suffix={`/ ${records.length}`}
|
|
|
|
|
|
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
|
|
|
|
|
/>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Progress
|
|
|
|
|
|
percent={
|
|
|
|
|
|
records.length > 0
|
|
|
|
|
|
? Math.round(
|
|
|
|
|
|
(records.filter(r => r.status !== 'pending').length / records.length) * 100
|
|
|
|
|
|
)
|
|
|
|
|
|
: 0
|
|
|
|
|
|
}
|
2026-02-26 11:24:00 +08:00
|
|
|
|
strokeColor={{ from: '#1890ff', to: '#52c41a' }}
|
|
|
|
|
|
style={{ marginTop: 12 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<style>{`
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
|
from { opacity: 0; transform: translateY(-10px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
`}</style>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="设备盘点"
|
|
|
|
|
|
open={checkModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2026-02-26 11:24:00 +08:00
|
|
|
|
onCancel={() => setCheckModalVisible(false)}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={600}
|
|
|
|
|
|
>
|
|
|
|
|
|
{currentRecord && (
|
|
|
|
|
|
<Form form={form} layout="vertical" onFinish={handleSubmitCheck}>
|
|
|
|
|
|
<Descriptions bordered size="small" column={2} style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Descriptions.Item label="设备ID">{currentRecord.deviceId}</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="设备名称">{currentRecord.deviceName}</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="系统序列号">{currentRecord.serialNumber}</Descriptions.Item>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Descriptions.Item label="系统位置">
|
|
|
|
|
|
{currentRecord.displayLocation ||
|
|
|
|
|
|
`${currentRecord.rackId} - U${currentRecord.position}`}
|
|
|
|
|
|
</Descriptions.Item>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
</Descriptions>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Form.Item name="actualSerialNumber" label="实际序列号">
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<Input placeholder="请输入实际序列号" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Form.Item name="actualRackId" label="实际机柜">
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<Input placeholder="请输入实际机柜ID" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Form.Item name="actualPosition" label="实际位置(U)">
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<Input type="number" placeholder="请输入实际位置" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="status"
|
|
|
|
|
|
label="盘点结果"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择盘点结果' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select placeholder="请选择盘点结果">
|
|
|
|
|
|
<Select.Option value="normal">
|
|
|
|
|
|
<Badge status="success" text="正常" />
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
<Select.Option value="abnormal">
|
|
|
|
|
|
<Badge status="error" text="异常" />
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Form.Item name="remark" label="备注">
|
2026-02-26 11:24:00 +08:00
|
|
|
|
<Input.TextArea rows={2} placeholder="请输入备注(如有异常请说明原因)" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button onClick={() => setCheckModalVisible(false)}>取消</Button>
|
|
|
|
|
|
<Button type="primary" htmlType="submit">
|
|
|
|
|
|
提交
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
2026-03-06 15:41:07 +08:00
|
|
|
|
|
|
|
|
|
|
<Modal
|
2026-03-10 09:20:31 +08:00
|
|
|
|
title={null}
|
2026-03-06 15:41:07 +08:00
|
|
|
|
open={quickAddModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2026-03-06 15:41:07 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setQuickAddModalVisible(false);
|
|
|
|
|
|
setSelectedRoomId(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
2026-03-10 09:20:31 +08:00
|
|
|
|
width={640}
|
2026-03-06 15:41:07 +08:00
|
|
|
|
centered
|
2026-03-10 09:20:31 +08:00
|
|
|
|
styles={{
|
|
|
|
|
|
body: { padding: 0 },
|
|
|
|
|
|
}}
|
2026-03-06 15:41:07 +08:00
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
padding: '24px 32px',
|
|
|
|
|
|
borderRadius: `${designTokens.borderRadius.large} ${designTokens.borderRadius.large} 0 0`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 48,
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: 'rgba(255, 255, 255, 0.2)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<PlusOutlined style={{ fontSize: 24, color: '#fff' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{ fontSize: designTokens.typography['2xl'], fontWeight: 600, color: '#fff' }}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
快速添加设备
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
color: 'rgba(255, 255, 255, 0.85)',
|
|
|
|
|
|
marginTop: 4,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
将扫描到的设备添加到系统
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-06 15:41:07 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<div style={{ padding: '24px 32px' }}>
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={quickAddForm}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
onFinish={handleQuickAddDevice}
|
|
|
|
|
|
requiredMark="optional"
|
2026-03-06 15:41:07 +08:00
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{/* 序列号显示 - 突出显示 */}
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="serialNumber"
|
|
|
|
|
|
label={
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
设备序列号
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
rules={[{ required: true, message: '请输入序列号' }]}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="请输入设备序列号"
|
|
|
|
|
|
disabled
|
2026-03-10 09:20:31 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
fontSize: designTokens.typography.md,
|
|
|
|
|
|
fontFamily: 'Consolas, Monaco, monospace',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
background: designTokens.colors.background.tertiary,
|
|
|
|
|
|
border: `2px solid ${designTokens.colors.border.medium}`,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
2026-03-06 15:41:07 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{/* 位置信息区域 */}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.background.secondary,
|
|
|
|
|
|
padding: 20,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
2026-03-10 09:20:31 +08:00
|
|
|
|
marginBottom: 16,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.text.secondary,
|
|
|
|
|
|
marginBottom: 16,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: 8,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 4,
|
|
|
|
|
|
height: 16,
|
|
|
|
|
|
background: designTokens.colors.primary.main,
|
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
|
}}
|
|
|
|
|
|
></span>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
位置信息(可选)
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col xs={24} sm={12}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Form.Item name="roomId" label="所属机房" style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="请选择机房"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
showSearch
|
2026-03-10 09:20:31 +08:00
|
|
|
|
optionFilterProp="children"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.medium }}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onChange={value => {
|
2026-03-10 09:20:31 +08:00
|
|
|
|
setSelectedRoomId(value);
|
|
|
|
|
|
quickAddForm.setFieldsValue({ rackId: undefined });
|
|
|
|
|
|
}}
|
|
|
|
|
|
suffixIcon={
|
|
|
|
|
|
selectedRoomId ? (
|
|
|
|
|
|
<Tag color="blue" style={{ margin: 0 }}>
|
|
|
|
|
|
{rooms.find(r => r.roomId === selectedRoomId)?.name}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{rooms.length > 0 ? (
|
|
|
|
|
|
rooms.map(room => (
|
|
|
|
|
|
<Select.Option key={room.roomId} value={room.roomId}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
|
<span style={{ fontSize: 16 }}>🏢</span>
|
|
|
|
|
|
{room.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Select.Option value="" disabled>
|
|
|
|
|
|
暂无机房数据
|
|
|
|
|
|
</Select.Option>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={12}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Form.Item name="rackId" label="所属机柜" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
placeholder={selectedRoomId ? '请选择机柜' : '请先选择机房'}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
showSearch
|
2026-03-10 09:20:31 +08:00
|
|
|
|
optionFilterProp="children"
|
|
|
|
|
|
disabled={!selectedRoomId}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.medium }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectedRoomId ? (
|
|
|
|
|
|
filteredRacks.length > 0 ? (
|
|
|
|
|
|
filteredRacks.map(rack => (
|
|
|
|
|
|
<Select.Option key={rack.rackId} value={rack.rackId}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
|
<span style={{ fontSize: 16 }}>🗄️</span>
|
|
|
|
|
|
{rack.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Select.Option value="" disabled>
|
|
|
|
|
|
该机房下无机柜
|
|
|
|
|
|
</Select.Option>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
)
|
|
|
|
|
|
) : (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Select.Option value="" disabled>
|
|
|
|
|
|
请先选择机房
|
|
|
|
|
|
</Select.Option>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</div>
|
2026-03-06 15:41:07 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{/* 设备详细信息 */}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.background.secondary,
|
|
|
|
|
|
padding: 20,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
marginBottom: 24,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: designTokens.typography.sm,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.text.secondary,
|
|
|
|
|
|
marginBottom: 16,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: 8,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 4,
|
|
|
|
|
|
height: 16,
|
|
|
|
|
|
background: designTokens.colors.primary.main,
|
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
|
}}
|
|
|
|
|
|
></span>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
设备详细信息
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
|
{renderQuickAddFormFields()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 操作按钮 */}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
gap: 12,
|
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
|
paddingTop: 8,
|
|
|
|
|
|
borderTop: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
2026-03-10 09:20:31 +08:00
|
|
|
|
size="large"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setQuickAddModalVisible(false);
|
|
|
|
|
|
setSelectedRoomId(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
minWidth: 100,
|
|
|
|
|
|
}}
|
2026-03-06 15:41:07 +08:00
|
|
|
|
>
|
2026-03-10 09:20:31 +08:00
|
|
|
|
取消
|
2026-03-06 15:41:07 +08:00
|
|
|
|
</Button>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
2026-03-10 09:20:31 +08:00
|
|
|
|
size="large"
|
2026-03-27 19:12:16 +08:00
|
|
|
|
htmlType="submit"
|
2026-03-10 09:20:31 +08:00
|
|
|
|
loading={quickAddLoading}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
minWidth: 140,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
确认添加
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
2026-03-06 15:41:07 +08:00
|
|
|
|
</Modal>
|
2026-02-26 11:24:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default InventoryTaskExecution;
|