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'; 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 = ; break; case 'textarea': formItem = ; break; case 'boolean': formItem = ; break; case 'date': formItem = ; break; case 'datetime': formItem = ; break; case 'select': const selectOptions = options && Array.isArray(options) ? options : []; formItem = ( ); break; case 'device': formItem = ( ); break; default: formItem = ; } return ( {formItem} ); }, [devices]); const tableColumns = useMemo(() => { const baseColumns = [ { title: '工单编号', dataIndex: 'ticketId', key: 'ticketId', width: 150, fixed: 'left' }, { title: '标题', dataIndex: 'title', key: 'title', width: 200, ellipsis: true }, { title: '设备信息', key: 'deviceInfo', width: 180, render: (_, record) => (
{record.deviceName || '-'}
{record.serialNumber || '-'}
) }, { title: '故障分类', dataIndex: 'faultCategory', key: 'faultCategory', width: 120, render: (value) => value ? {value} : '-' }, { title: '优先级', dataIndex: 'priority', key: 'priority', width: 80, render: (value) => {getPriorityText(value)} }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (value) => {getStatusText(value)} }, { title: '报告人', dataIndex: 'reporterName', key: 'reporterName', width: 100 }, { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, render: (value) => value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-' } ]; const customFieldColumns = ticketFields .filter(field => !BUILTIN_TICKET_FIELDS.includes(field.fieldName) && field.visible) .map(field => ({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: 120, render: (value) => { if (value === null || value === undefined) return '-'; if (field.fieldType === 'boolean') { return ; } if (field.fieldType === 'date' || field.fieldType === 'datetime') { return value ? dayjs(value).format(field.fieldType === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm') : '-'; } if (field.fieldType === 'select' && Array.isArray(field.options)) { const option = field.options.find(opt => opt.value === value); return option ? option.label : value; } return String(value); } })); const actionColumn = { title: '操作', key: 'action', width: 100, fixed: 'right', render: (_, record) => ( }>
{ localStorage.setItem('ticketVisibleColumns', JSON.stringify(visibleColumns)); } }} />
{renderFormItems()}
setProcessingModalVisible(false)} footer={null} width={600} >
{selectedTicket?.ticketId} {selectedTicket?.title} {selectedTicket?.deviceName} {selectedTicket?.description}