import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
Table,
Button,
Modal,
Form,
Input,
Select,
DatePicker,
message,
Card,
Space,
Tag,
Dropdown,
Menu,
Tabs,
Timeline,
Descriptions,
Checkbox,
Popover,
InputNumber,
Switch,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
EyeOutlined,
MoreOutlined,
UserOutlined,
ToolOutlined,
CheckCircleOutlined,
SyncOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
SettingOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
// 安全地从 localStorage 获取用户信息
const getUserFromStorage = () => {
try {
const userStr = localStorage.getItem('user');
return userStr ? JSON.parse(userStr) : {};
} catch (e) {
console.error('解析用户信息失败:', e);
return {};
}
};
const { Option } = Select;
const { RangePicker } = DatePicker;
const { TextArea } = Input;
const { TabPane } = Tabs;
const BUILTIN_TICKET_FIELDS = [
'ticketId',
'title',
'deviceId',
'deviceName',
'deviceModel',
'serialNumber',
'faultCategory',
'faultSubCategory',
'priority',
'status',
'description',
'expectedCompletionDate',
'reporterId',
'reporterName',
'assigneeId',
'assigneeName',
'location',
'resolution',
'completionDate',
'evaluation',
'evaluationRating',
'attachments',
'tags',
'notes',
'result',
'solution',
'usedParts',
];
const DEFAULT_TICKET_FIELDS = [
{
fieldName: 'title',
displayName: '标题',
fieldType: 'string',
required: true,
order: 1,
visible: true,
},
{
fieldName: 'deviceId',
displayName: '关联设备',
fieldType: 'device',
required: false,
order: 2,
visible: true,
},
{
fieldName: 'deviceName',
displayName: '设备名称',
fieldType: 'string',
required: false,
order: 3,
visible: true,
},
{
fieldName: 'serialNumber',
displayName: '设备序列号',
fieldType: 'string',
required: false,
order: 4,
visible: true,
},
{
fieldName: 'faultCategory',
displayName: '故障分类',
fieldType: 'select',
required: true,
order: 5,
visible: true,
options: [],
},
{
fieldName: 'priority',
displayName: '优先级',
fieldType: 'select',
required: true,
order: 6,
visible: true,
options: [
{ value: 'low', label: '低' },
{ value: 'medium', label: '中' },
{ value: 'high', label: '高' },
{ value: 'urgent', label: '紧急' },
],
},
{
fieldName: 'description',
displayName: '故障描述',
fieldType: 'textarea',
required: true,
order: 7,
visible: true,
},
{
fieldName: 'expectedCompletionDate',
displayName: '期望完成时间',
fieldType: 'datetime',
required: false,
order: 8,
visible: true,
},
{
fieldName: 'resolution',
displayName: '解决方案',
fieldType: 'textarea',
required: false,
order: 9,
visible: true,
},
{
fieldName: 'notes',
displayName: '备注',
fieldType: 'textarea',
required: false,
order: 10,
visible: true,
},
];
const getStatusColor = status => {
const colors = {
pending: 'orange',
in_progress: 'processing',
completed: 'green',
closed: 'default',
};
return colors[status] || 'default';
};
const getStatusText = status => {
const texts = {
pending: '待处理',
in_progress: '处理中',
completed: '已完成',
closed: '已关闭',
};
return texts[status] || status;
};
const getPriorityColor = priority => {
const colors = {
low: 'green',
medium: 'orange',
high: 'red',
urgent: 'magenta',
};
return colors[priority] || 'default';
};
const getPriorityText = priority => {
const texts = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急',
};
return texts[priority] || priority;
};
const generateTicketId = () => {
const prefix = 'TKT';
const timestamp = dayjs().format('YYYYMMDDHHmmss');
const random = Math.floor(Math.random() * 1000)
.toString()
.padStart(3, '0');
return `${prefix}${timestamp}${random}`;
};
function TicketManagement() {
const [tickets, setTickets] = useState([]);
const [devices, setDevices] = useState([]);
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [processingModalVisible, setProcessingModalVisible] = useState(false);
const [editingTicket, setEditingTicket] = useState(null);
const [selectedTicket, setSelectedTicket] = useState(null);
const [operationRecords, setOperationRecords] = useState([]);
const [form] = Form.useForm();
const [processForm] = Form.useForm();
const [searchForm] = Form.useForm();
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
pageSizeOptions: ['10', '20', '30', '50'],
showSizeChanger: true,
showTotal: total => `共 ${total} 条记录`,
});
const [searchFilters, setSearchFilters] = useState({});
const [deviceSource, setDeviceSource] = useState('select');
const [ticketFields, setTicketFields] = useState(DEFAULT_TICKET_FIELDS);
const [loadingFields, setLoadingFields] = useState(true);
const fetchTickets = useCallback(
async (page = 1, pageSize = 10, filters = {}) => {
try {
setLoading(true);
const params = {
page,
pageSize,
...searchFilters,
...filters,
};
const response = await axios.get('/api/tickets', { params });
const { tickets: ticketList, total } = response.data;
const processedTickets = ticketList.map(ticket => {
const processed = { ...ticket };
if (ticket.metadata && typeof ticket.metadata === 'object') {
Object.entries(ticket.metadata).forEach(([key, value]) => {
processed[key] = value;
});
}
return processed;
});
setTickets(processedTickets);
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
} catch (error) {
message.error('获取工单列表失败');
console.error('获取工单列表失败:', error);
} finally {
setLoading(false);
}
},
[searchFilters]
);
const fetchDevices = useCallback(async () => {
try {
const response = await axios.get('/api/devices', { params: { pageSize: 1000 } });
setDevices(response.data.devices || []);
} catch (error) {
console.error('获取设备列表失败:', error);
}
}, []);
const fetchCategories = useCallback(async () => {
try {
const response = await axios.get('/api/ticket-categories');
const categoryOptions = (response.data || []).map(cat => ({
value: cat.name,
label: cat.name,
}));
setCategories(response.data || []);
setTicketFields(prev =>
prev.map(field =>
field.fieldName === 'faultCategory' ? { ...field, options: categoryOptions } : field
)
);
} catch (error) {
console.error('获取分类列表失败:', error);
}
}, []);
const fetchTicketFields = useCallback(async () => {
try {
setLoadingFields(true);
const response = await axios.get('/api/ticket-fields');
const sortedFields = response.data.sort((a, b) => a.order - b.order);
setTicketFields(sortedFields);
} catch (error) {
console.error('获取工单字段配置失败:', error);
setTicketFields(DEFAULT_TICKET_FIELDS);
} finally {
setLoadingFields(false);
}
}, []);
useEffect(() => {
fetchTickets();
fetchDevices();
fetchCategories();
fetchTicketFields();
}, [fetchTickets, fetchDevices, fetchCategories, fetchTicketFields]);
const renderFormItem = useCallback(
field => {
const { fieldName, displayName, fieldType, required, options, placeholder } = field;
const rules = required ? [{ required: true, message: `请选择或输入${displayName}` }] : [];
let formItem;
switch (fieldType) {
case 'string':
formItem = ;
break;
case 'number':
formItem = (