import React, { useState, useEffect, useCallback } from 'react';
import {
Table,
Button,
Modal,
Form,
Input,
Select,
InputNumber,
DatePicker,
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,
PlusOutlined,
} from '@ant-design/icons';
import api from '../api';
import dayjs from 'dayjs';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { designTokens } from '../config/theme';
import CloseButton from '../components/CloseButton';
const InventoryTaskExecution = () => {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
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 });
const [quickAddModalVisible, setQuickAddModalVisible] = useState(false);
const [quickAddForm] = Form.useForm();
const [quickAddLoading, setQuickAddLoading] = useState(false);
const [rooms, setRooms] = useState([]);
const [racks, setRacks] = useState([]);
const [deviceFields, setDeviceFields] = useState(null); // 初始值改为 null,表示未加载
const [selectedRoomId, setSelectedRoomId] = useState(null);
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]);
const fetchTaskRecords = async taskId => {
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;
});
const handleBatchCheck = async status => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择要盘点的记录');
return;
}
setBatchLoading(true);
try {
const promises = selectedRowKeys.map(recordId =>
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([]);
}
};
const handleQuickCheckAll = async status => {
const pendingRecords = filteredRecords.filter(r => r.status === 'pending');
if (pendingRecords.length === 0) {
message.info('没有待盘点的记录');
return;
}
setBatchLoading(true);
try {
const promises = pendingRecords.map(record =>
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);
}
};
const handleScan = async sn => {
if (!sn || sn.trim() === '') return;
const trimmedSN = sn.trim().toUpperCase();
const matchedRecord = records.find(
r => r.serialNumber && r.serialNumber.toUpperCase() === trimmedSN
);
if (!matchedRecord) {
setScanResult({ success: false, message: `未找到序列号为 "${sn}" 的设备`, sn: trimmedSN });
setScanModalVisible(true);
return;
}
if (matchedRecord.status !== 'pending') {
setScanResult({
success: false,
message: `设备 "${matchedRecord.deviceName}" 已盘点`,
record: matchedRecord,
sn: trimmedSN,
});
setScanModalVisible(true);
return;
}
try {
await api.post(`/inventory/records/${matchedRecord.recordId}/check`, {
status: 'normal',
actualSerialNumber: matchedRecord.serialNumber,
checkedAt: new Date().toISOString(),
});
setScanResult({
success: true,
message: `设备 "${matchedRecord.deviceName}" 盘点成功!`,
record: matchedRecord,
sn: trimmedSN,
});
setScanModalVisible(true);
if (currentTask) {
fetchTaskRecords(currentTask.taskId);
}
} catch (error) {
message.error('盘点失败');
}
};
const handleScanInput = e => {
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('');
}
}
};
const handleScanKeyPress = e => {
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]);
const defaultDeviceFields = [
{
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,
},
];
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);
}
};
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 {
const res = await api.get('/racks', { params: { pageSize: 1000 } });
setRacks(res.data.racks || res.data || []);
} catch (error) {
console.error('获取机柜列表失败', error);
}
};
useEffect(() => {
fetchRooms();
fetchRacks();
fetchDeviceFields();
}, []);
const filteredRacks = selectedRoomId
? racks.filter(rack => rack.roomId === selectedRoomId)
: racks;
const renderQuickAddFormField = field => {
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 (
);
}
if (fieldType === 'select') {
return (
);
}
if (fieldType === 'number') {
return (
);
}
if (fieldType === 'date') {
return (
);
}
if (fieldType === 'text' || fieldType === 'textarea') {
return (
);
}
return (
);
};
const renderQuickAddFormFields = () => {
// 如果字段配置未加载,使用默认配置
const fields = deviceFields || defaultDeviceFields;
// 排除机房和机柜字段(已在Modal中单独渲染)
const visibleFields = fields.filter(
f =>
f.visible !== false &&
f.fieldName !== 'serialNumber' &&
f.fieldName !== 'roomId' &&
f.fieldName !== 'rackId'
);
const sortedFields = visibleFields.sort((a, b) => a.order - b.order);
// 将字段分成两列
const leftFields = [];
const rightFields = [];
const fullFields = [];
sortedFields.forEach((field, index) => {
// 文本域类型独占一行
if (field.fieldType === 'text') {
fullFields.push(field);
} else {
// 其他字段交替排列
if (index % 2 === 0) {
leftFields.push(field);
} else {
rightFields.push(field);
}
}
});
return (
<>
{/* 两列布局的字段 */}
{leftFields.map(renderQuickAddFormField)}
{rightFields.map(renderQuickAddFormField)}
{/* 独占一行的字段(如备注) */}
{fullFields.map(renderQuickAddFormField)}
>
);
};
const handleQuickAddDevice = async values => {
if (!plan) {
message.error('盘点计划不存在');
return;
}
setQuickAddLoading(true);
try {
const { purchaseDate, warrantyExpiry, ...otherValues } = values;
const payload = {
taskId: currentTask?.taskId || null,
planId: plan.planId,
serialNumber: scanResult?.sn || values.serialNumber,
...otherValues,
purchaseDate: purchaseDate ? dayjs(purchaseDate).toISOString() : null,
warrantyExpiry: warrantyExpiry ? dayjs(warrantyExpiry).toISOString() : null,
};
const res = await api.post('/inventory/quick-add-device', payload);
message.success('设备已暂存!');
setQuickAddModalVisible(false);
setSelectedRoomId(null);
quickAddForm.resetFields();
setScanResult({
success: true,
message: `设备 "${res.data.pendingDevice.deviceName}" 已暂存,请前往「暂存设备」页面完善信息后同步到设备管理`,
pendingDevice: res.data.pendingDevice,
sn: res.data.pendingDevice.serialNumber,
});
} catch (error) {
message.error(error.response?.data?.error || '暂存设备失败');
} 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);
};
const handleCheck = record => {
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 || '标记失败');
}
};
const handleSubmitCheck = async values => {
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 || '提交失败');
}
};
const getStatusTag = status => {
const statusMap = {
pending: { color: 'default', text: '待盘点', icon: },
normal: { color: 'success', text: '正常', icon: },
abnormal: { color: 'error', text: '异常', icon: },
missed: { color: 'warning', text: '漏盘', icon: },
};
const config = statusMap[status] || statusMap.pending;
return (
{config.text}
);
};
const getAbnormalTypeTag = type => {
const typeMap = {
serial_mismatch: '序列号不符',
position_mismatch: '位置不符',
device_missing: '设备缺失',
extra_device: '多出设备',
};
return typeMap[type] || type;
};
const taskColumns = [
{
title: '任务信息',
key: 'taskInfo',
render: (_, record) => (
{record.targetName}
{record.targetType === 'room'
? '机房'
: record.targetType === 'rack'
? '机柜'
: '全部设备'}
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: status => {
const statusMap = {
pending: ,
in_progress: ,
completed: ,
skipped: ,
};
return statusMap[status] || status;
},
},
{
title: '设备进度',
key: 'progress',
width: 150,
render: (_, record) => (