diff --git a/backend/initTicketFields.js b/backend/initTicketFields.js new file mode 100644 index 0000000..dc67864 --- /dev/null +++ b/backend/initTicketFields.js @@ -0,0 +1,148 @@ +const TicketField = require('./models/TicketField'); +const { sequelize } = require('./db'); + +const defaultTicketFields = [ + { + fieldName: 'ticketId', + displayName: '工单编号', + fieldType: 'string', + required: true, + order: 1, + visible: true + }, + { + fieldName: 'title', + displayName: '标题', + fieldType: 'string', + required: true, + 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: 'status', + displayName: '状态', + fieldType: 'select', + required: true, + order: 7, + visible: true, + options: [ + { value: 'pending', label: '待处理' }, + { value: 'in_progress', label: '处理中' }, + { value: 'completed', label: '已完成' }, + { value: 'closed', label: '已关闭' } + ] + }, + { + fieldName: 'reporterName', + displayName: '报告人', + fieldType: 'string', + required: false, + order: 8, + visible: true + }, + { + fieldName: 'createdAt', + displayName: '创建时间', + fieldType: 'datetime', + required: false, + order: 9, + visible: true + }, + { + fieldName: 'expectedCompletionDate', + displayName: '期望完成时间', + fieldType: 'datetime', + required: false, + order: 10, + visible: true + }, + { + fieldName: 'completionDate', + displayName: '完成时间', + fieldType: 'datetime', + required: false, + order: 11, + visible: true + }, + { + fieldName: 'description', + displayName: '故障描述', + fieldType: 'textarea', + required: false, + order: 12, + visible: true, + placeholder: '请详细描述故障情况' + }, + { + fieldName: 'resolution', + displayName: '解决方案', + fieldType: 'textarea', + required: false, + order: 13, + visible: true, + placeholder: '请输入解决方案' + }, + { + fieldName: 'location', + displayName: '位置', + fieldType: 'string', + required: false, + order: 14, + visible: false + } +]; + +async function initializeTicketFields() { + try { + const count = await TicketField.count(); + if (count === 0) { + await TicketField.bulkCreate(defaultTicketFields); + console.log('工单字段初始化完成'); + } else { + console.log('工单字段已存在,跳过初始化'); + } + } catch (error) { + console.error('工单字段初始化失败:', error); + } +} + +module.exports = initializeTicketFields; diff --git a/backend/models/TicketField.js b/backend/models/TicketField.js new file mode 100644 index 0000000..e7300ec --- /dev/null +++ b/backend/models/TicketField.js @@ -0,0 +1,53 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db'); + +const TicketField = sequelize.define('TicketField', { + fieldId: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false + }, + fieldName: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + displayName: { + type: DataTypes.STRING, + allowNull: false + }, + fieldType: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: 'string' + }, + required: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + options: { + type: DataTypes.JSON, + allowNull: true + }, + order: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + visible: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }, + placeholder: { + type: DataTypes.STRING, + allowNull: true + } +}, { + tableName: 'ticketFields', + timestamps: true +}); + +module.exports = TicketField; diff --git a/backend/routes/ticketFields.js b/backend/routes/ticketFields.js new file mode 100644 index 0000000..95ddd51 --- /dev/null +++ b/backend/routes/ticketFields.js @@ -0,0 +1,95 @@ +const express = require('express'); +const router = express.Router(); +const TicketField = require('../models/TicketField'); + +router.get('/', async (req, res) => { + try { + const fields = await TicketField.findAll({ + order: [['order', 'ASC']] + }); + res.json(fields); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.get('/:fieldId', async (req, res) => { + try { + const field = await TicketField.findByPk(req.params.fieldId); + if (!field) { + return res.status(404).json({ error: '字段不存在' }); + } + res.json(field); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.post('/', async (req, res) => { + try { + const field = await TicketField.create(req.body); + res.status(201).json(field); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +router.put('/:fieldId', async (req, res) => { + try { + const [updated] = await TicketField.update(req.body, { + where: { fieldId: req.params.fieldId } + }); + if (updated) { + const updatedField = await TicketField.findByPk(req.params.fieldId); + res.json(updatedField); + } else { + res.status(404).json({ error: '字段不存在' }); + } + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +router.delete('/:fieldId', async (req, res) => { + try { + const deleted = await TicketField.destroy({ + where: { fieldId: req.params.fieldId } + }); + if (deleted) { + res.status(204).json(); + } else { + res.status(404).json({ error: '字段不存在' }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.post('/config', async (req, res) => { + try { + const fieldConfigs = req.body; + + if (!Array.isArray(fieldConfigs)) { + return res.status(400).json({ error: '输入必须是数组' }); + } + + const updatedFields = []; + for (const config of fieldConfigs) { + const [updated] = await TicketField.update( + { visible: config.visible }, + { where: { fieldName: config.fieldName } } + ); + + if (updated) { + const updatedField = await TicketField.findOne({ where: { fieldName: config.fieldName } }); + updatedFields.push(updatedField); + } + } + + res.json(updatedFields); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index 4530160..1bb564d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -31,6 +31,10 @@ sequelize.authenticate() // 同步完成后初始化设备字段 return require('./initDeviceFields')(); }) + .then(() => { + // 初始化工单字段 + return require('./initTicketFields')(); + }) .then(() => { // 初始化工单模型关联 return require('./models/ticketIndex').initializeModels(); @@ -53,6 +57,7 @@ const loginHistoryRoutes = require('./routes/loginHistory'); const operationLogsRoutes = require('./routes/operationLogs'); const ticketRoutes = require('./routes/tickets'); const ticketCategoryRoutes = require('./routes/ticketCategories'); +const ticketFieldRoutes = require('./routes/ticketFields'); // 使用路由 app.use('/api/devices', deviceRoutes); @@ -70,6 +75,7 @@ app.use('/api/login-history', loginHistoryRoutes); app.use('/api/operation-logs', operationLogsRoutes); app.use('/api/tickets', ticketRoutes); app.use('/api/ticket-categories', ticketCategoryRoutes); +app.use('/api/ticket-fields', ticketFieldRoutes); // 静态文件服务 app.use('/uploads', express.static('uploads')); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d78fbdd..f96fe8c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,6 +22,7 @@ const Login = lazy(() => import('./pages/Login')); const TicketManagement = lazy(() => import('./pages/TicketManagement')); const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManagement')); const TicketStatistics = lazy(() => import('./pages/TicketStatistics')); +const TicketFieldManagement = lazy(() => import('./pages/TicketFieldManagement')); const { Header, Content, Sider } = Layout; @@ -247,6 +248,11 @@ const AppLayout = ({ children }) => { icon: , label: 统计报表, }, + { + key: 'ticket-fields', + icon: , + label: 字段管理, + }, ], }, ]} @@ -449,6 +455,14 @@ function App() { } /> + + + + } + /> } /> diff --git a/frontend/src/pages/TicketFieldManagement.jsx b/frontend/src/pages/TicketFieldManagement.jsx new file mode 100644 index 0000000..47c4011 --- /dev/null +++ b/frontend/src/pages/TicketFieldManagement.jsx @@ -0,0 +1,269 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Switch } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import axios from 'axios'; + +const { Option } = Select; + +function TicketFieldManagement() { + const [fields, setFields] = useState([]); + const [loading, setLoading] = useState(true); + const [modalVisible, setModalVisible] = useState(false); + const [editingField, setEditingField] = useState(null); + const [form] = Form.useForm(); + + const fetchFields = async () => { + try { + setLoading(true); + const response = await axios.get('/api/ticket-fields'); + setFields(response.data.sort((a, b) => a.order - b.order)); + } catch (error) { + message.error('获取字段列表失败'); + console.error('获取字段列表失败:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchFields(); + }, []); + + const showModal = (field = null) => { + setEditingField(field); + if (field) { + const fieldData = { + ...field, + options: field.options ? JSON.stringify(field.options, null, 2) : '' + }; + form.setFieldsValue(fieldData); + } else { + form.resetFields(); + } + setModalVisible(true); + }; + + const handleCancel = () => { + setModalVisible(false); + setEditingField(null); + }; + + const handleSubmit = async (values) => { + try { + const fieldData = { + ...values, + options: values.options ? JSON.parse(values.options || '[]') : null + }; + + if (editingField) { + await axios.put(`/api/ticket-fields/${editingField.fieldId}`, fieldData); + message.success('字段更新成功'); + } else { + await axios.post('/api/ticket-fields', fieldData); + message.success('字段创建成功'); + } + + setModalVisible(false); + fetchFields(); + setEditingField(null); + } catch (error) { + message.error(editingField ? '字段更新失败' : '字段创建失败'); + console.error(error); + } + }; + + const handleDelete = async (fieldId) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除这个字段吗?', + okText: '删除', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + await axios.delete(`/api/ticket-fields/${fieldId}`); + message.success('字段删除成功'); + fetchFields(); + } catch (error) { + message.error('字段删除失败'); + console.error('字段删除失败:', error); + } + } + }); + }; + + const columns = [ + { + title: '字段名称', + dataIndex: 'fieldName', + key: 'fieldName', + }, + { + title: '显示名称', + dataIndex: 'displayName', + key: 'displayName', + }, + { + title: '字段类型', + dataIndex: 'fieldType', + key: 'fieldType', + render: (type) => { + const typeMap = { + string: '文本', + number: '数字', + boolean: '布尔值', + select: '下拉选择', + date: '日期', + datetime: '日期时间', + textarea: '多行文本', + device: '设备选择' + }; + return typeMap[type] || type; + } + }, + { + title: '必填', + dataIndex: 'required', + key: 'required', + render: (required) => ( + + ) + }, + { + title: '可见', + dataIndex: 'visible', + key: 'visible', + render: (visible) => ( + + ) + }, + { + title: '顺序', + dataIndex: 'order', + key: 'order', + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + } onClick={() => showModal(record)} size="small"> + 编辑 + + } onClick={() => handleDelete(record.fieldId)} size="small"> + 删除 + + + ), + }, + ]; + + return ( + + } onClick={() => showModal()}> + 添加字段 + + }> + + + + + + + + + + + + + + + + 文本 + 数字 + 布尔值 + 下拉选择 + 日期 + 日期时间 + 多行文本 + 设备选择 + + + + + + + + + + + + + + + + + + + + + + 取消 + + 确定 + + + + + + + ); +} + +export default TicketFieldManagement; diff --git a/frontend/src/pages/TicketManagement.jsx b/frontend/src/pages/TicketManagement.jsx index 2af3f87..05b8ae6 100644 --- a/frontend/src/pages/TicketManagement.jsx +++ b/frontend/src/pages/TicketManagement.jsx @@ -1,6 +1,6 @@ -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 } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined, MoreOutlined, UserOutlined, ToolOutlined, CheckCircleOutlined, SyncOutlined, ClockCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; +import React, { useState, useEffect, useCallback } 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'; @@ -9,6 +9,29 @@ 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', @@ -75,6 +98,8 @@ function TicketManagement() { 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 { @@ -89,7 +114,17 @@ function TicketManagement() { const response = await axios.get('/api/tickets', { params }); const { tickets: ticketList, total } = response.data; - setTickets(ticketList); + 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('获取工单列表失败'); @@ -111,12 +146,165 @@ function TicketManagement() { 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 = ( + + {selectOptions.map((opt, idx) => ( + {opt.label} + ))} + + ); + break; + case 'device': + formItem = ( + + {devices.map(device => ( + + {device.name} - {device.serialNumber} + + ))} + + ); + break; + default: + formItem = ; + } + + return ( + + {formItem} + + ); + }, [devices]); + + const getTableColumns = useCallback(() => { + 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); + } + })); + + return [...baseColumns, ...customFieldColumns]; + }, [ticketFields]); + const fetchTicketDetail = useCallback(async (ticketId) => { try { const [ticketRes, operationsRes] = await Promise.all([ @@ -133,12 +321,6 @@ function TicketManagement() { } }, []); - useEffect(() => { - fetchTickets(); - fetchDevices(); - fetchCategories(); - }, [fetchTickets, fetchDevices, fetchCategories]); - const showModal = useCallback((ticket = null) => { setEditingTicket(ticket); if (ticket) { @@ -157,6 +339,11 @@ function TicketManagement() { ticketData.deviceName = ticket.deviceName; ticketData.serialNumber = ticket.serialNumber; } + if (ticket.metadata && typeof ticket.metadata === 'object') { + Object.entries(ticket.metadata).forEach(([key, value]) => { + ticketData[key] = value; + }); + } form.setFieldsValue(ticketData); } else { form.resetFields(); @@ -175,13 +362,29 @@ function TicketManagement() { const ticketData = { ...values, expectedCompletionDate: values.expectedCompletionDate ? values.expectedCompletionDate.format('YYYY-MM-DD HH:mm:ss') : null, - completionDate: values.completionDate ? values.completionDate.format('YYYY-MM-DD HH:mm:ss') : null + completionDate: values.completionDate ? values.completionDate.format('YYYY-MM-DD HH:mm:ss') : null, + metadata: {} }; + ticketFields.forEach(field => { + if (!BUILTIN_TICKET_FIELDS.includes(field.fieldName)) { + if (values[field.fieldName] !== undefined) { + ticketData.metadata[field.fieldName] = values[field.fieldName]; + } + delete ticketData[field.fieldName]; + } + }); + if (deviceSource === 'manual') { ticketData.deviceId = null; ticketData.deviceName = values.deviceName; ticketData.serialNumber = values.serialNumber; + } else { + const selectedDevice = devices.find(d => d.deviceId === values.deviceId); + if (selectedDevice) { + ticketData.deviceName = selectedDevice.name; + ticketData.serialNumber = selectedDevice.serialNumber; + } } if (editingTicket) { @@ -203,7 +406,7 @@ function TicketManagement() { message.error(editingTicket ? '工单更新失败' : '工单创建失败'); console.error(error); } - }, [editingTicket, fetchTickets, deviceSource]); + }, [editingTicket, fetchTickets, deviceSource, ticketFields, devices]); const handleDelete = useCallback(async (ticketId) => { Modal.confirm({ @@ -278,150 +481,76 @@ function TicketManagement() { fetchTickets(paginationInfo.current, paginationInfo.pageSize, searchFilters); }, [fetchTickets, searchFilters]); - const columns = useMemo(() => [ - { - title: '工单编号', - dataIndex: 'ticketId', - key: 'ticketId', - width: 150, - render: (text, record) => ( - fetchTicketDetail(text)}> - {text} - - ) - }, - { - 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: (text) => text ? {text} : '-' - }, - { - title: '优先级', - dataIndex: 'priority', - key: 'priority', - width: 80, - render: (priority) => ( - - {getPriorityText(priority)} - - ) - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 100, - render: (status) => ( - - {getStatusText(status)} - - ) - }, - { - title: '报告人', - dataIndex: ['reporter', 'username'], - key: 'reporter', - width: 100, - render: (text) => text || '-' - }, - { - title: '创建时间', - dataIndex: 'createdAt', - key: 'createdAt', - width: 160, - render: (text) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-' - }, - { - title: '期望完成时间', - dataIndex: 'expectedCompletionDate', - key: 'expectedCompletionDate', - width: 160, - render: (text) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-' - }, - { - title: '操作', - key: 'action', - width: 150, - fixed: 'right', - render: (_, record) => ( - - } - onClick={() => fetchTicketDetail(record.ticketId)} - > - 详情 - - {record.status === 'pending' ? ( - } - onClick={() => handleProcess(record)} - > - 处理 - - ) : null} - - } onClick={() => showModal(record)}> - 编辑 - - {(record.status === 'pending' || record.status === 'in_progress') && ( - } - onClick={() => handleStatusChange(record.ticketId, 'completed')} - > - 完成工单 - - )} - {record.status === 'completed' && ( - } - onClick={() => handleStatusChange(record.ticketId, 'closed')} - > - 关闭工单 - - )} - } - danger - onClick={() => handleDelete(record.ticketId)} - > - 删除 - - - } - > - } /> - - - ) + const handleDeviceSourceChange = useCallback((value) => { + setDeviceSource(value); + }, []); + + const renderDeviceFormItems = useCallback(() => { + if (deviceSource === 'select') { + return ( + + + {devices.map(device => ( + + {device.name} - {device.serialNumber} + + ))} + + + ); } - ], [fetchTicketDetail, handleStatusChange, handleDelete]); + return ( + <> + + + + + + + > + ); + }, [deviceSource, devices]); + + const renderFormItems = useCallback(() => { + return ticketFields.map(field => { + if (field.fieldName === 'deviceId') { + return ( + + + + 从设备管理选择 + 手动输入 + + + {renderDeviceFormItems()} + + ); + } + return renderFormItem(field); + }); + }, [ticketFields, deviceSource, devices, handleDeviceSourceChange, renderDeviceFormItems, renderFormItem]); + + const getActionItems = useCallback((record) => ( + , label: '查看详情', onClick: () => fetchTicketDetail(record.ticketId) }, + { key: 'process', icon: , label: '处理工单', disabled: record.status === 'closed' || record.status === 'completed', + onClick: () => handleProcess(record) }, + { type: 'divider' }, + { key: 'pending', label: '标记为待处理', disabled: record.status !== 'pending', + onClick: () => handleStatusChange(record.ticketId, 'pending') }, + { key: 'in_progress', label: '标记为处理中', disabled: record.status !== 'pending', + onClick: () => handleStatusChange(record.ticketId, 'in_progress') }, + { key: 'completed', label: '标记为已完成', disabled: record.status === 'completed' || record.status === 'closed', + onClick: () => handleStatusChange(record.ticketId, 'completed') }, + { key: 'closed', label: '标记为已关闭', disabled: record.status === 'closed', + onClick: () => handleStatusChange(record.ticketId, 'closed') }, + { type: 'divider' }, + { key: 'delete', icon: , label: '删除工单', danger: true, + onClick: () => handleDelete(record.ticketId) } + ]} + /> + ), [fetchTicketDetail, handleProcess, handleStatusChange, handleDelete]); return ( @@ -468,13 +597,18 @@ function TicketManagement() { { + localStorage.setItem('ticketVisibleColumns', JSON.stringify(visibleColumns)); + } + }} /> @@ -486,67 +620,7 @@ function TicketManagement() { width={700} > - - - - - - - 从设备列表选择 - 手动输入设备信息 - - - - {deviceSource === 'select' ? ( - - - {devices.map(device => ( - - {device.name} - {device.serialNumber} - - ))} - - - ) : ( - <> - - - - - - - > - )} - - - - {categories.map(cat => ( - {cat.name} - ))} - - - - - - 低 - 中 - 高 - 紧急 - - - - - - - - - - - - - - - + {renderFormItems()} @@ -643,7 +717,7 @@ function TicketManagement() { {getStatusText(selectedTicket.status)} - {selectedTicket.reporter?.username || '-'} + {selectedTicket.reporterName} {selectedTicket.createdAt ? dayjs(selectedTicket.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}