From 0b00bf6f3da289199ecf3f42ebe4eaccc741c0ee Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Thu, 12 Feb 2026 13:27:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=BE=E5=A4=87=E5=B7=A5=E5=8D=95=E5=85=B3?= =?UTF-8?q?=E8=81=94=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现设备与工单的关联功能,包括: 1. 后端添加设备工单列表接口 2. 前端API添加设备工单相关方法 3. 设备详情页增加工单标签页 4. 设备管理页添加查看/创建工单按钮 5. 工单管理页支持从设备跳转查看/创建工单 --- backend/routes/devices.js | 38 ++++ frontend/src/api/index.js | 9 + .../src/components/DeviceDetailDrawer.jsx | 154 ++++++++++++- frontend/src/pages/DeviceManagement.jsx | 30 +++ frontend/src/pages/TicketManagement.jsx | 206 +++++++++++++++++- 5 files changed, 428 insertions(+), 9 deletions(-) diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 7fcd7ab..8ae7f57 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -950,6 +950,44 @@ router.get('/:deviceId', async (req, res) => { } }); +// 获取设备的工单列表 +router.get('/:deviceId/tickets', async (req, res) => { + try { + const { deviceId } = req.params; + const { status, page = 1, pageSize = 10 } = req.query; + const offset = (page - 1) * pageSize; + + // 检查设备是否存在 + const device = await Device.findByPk(deviceId); + if (!device) { + return res.status(404).json({ error: '设备不存在' }); + } + + // 构建查询条件 + const where = { deviceId }; + if (status && status !== 'all') { + where.status = status; + } + + // 查询工单列表 + const { count, rows: tickets } = await Ticket.findAndCountAll({ + where, + order: [['createdAt', 'DESC']], + offset: parseInt(offset), + limit: parseInt(pageSize) + }); + + res.json({ + data: tickets, + total: count, + page: parseInt(page), + pageSize: parseInt(pageSize) + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + // 更新设备 router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => { try { diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 79b6ca4..4ee3db5 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -125,6 +125,15 @@ export const operationLogAPI = { clear: data => api.delete('/operation-logs', { data }), }; +export const deviceAPI = { + list: params => api.get('/devices', { params }), + get: deviceId => api.get(`/devices/${deviceId}`), + create: data => api.post('/devices', data), + update: (deviceId, data) => api.put(`/devices/${deviceId}`, data), + delete: deviceId => api.delete(`/devices/${deviceId}`), + getTickets: (deviceId, params) => api.get(`/devices/${deviceId}/tickets`, { params }), +}; + export const ticketAPI = { list: params => api.get('/tickets', { params }), get: ticketId => api.get(`/tickets/${ticketId}`), diff --git a/frontend/src/components/DeviceDetailDrawer.jsx b/frontend/src/components/DeviceDetailDrawer.jsx index a7898e1..76a62e1 100644 --- a/frontend/src/components/DeviceDetailDrawer.jsx +++ b/frontend/src/components/DeviceDetailDrawer.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { Drawer, Tabs, @@ -10,6 +10,8 @@ import { Tooltip, Button, Popconfirm, + Table, + Badge, } from 'antd'; import { ApiOutlined, @@ -18,8 +20,12 @@ import { EditOutlined, PlusCircleOutlined, DeleteOutlined, + FileTextOutlined, + ToolOutlined, + EyeOutlined, } from '@ant-design/icons'; import NetworkCardPanel from './NetworkCardPanel'; +import { deviceAPI } from '../api'; const { Text, Title } = Typography; @@ -49,8 +55,121 @@ function DeviceDetailDrawer({ onDeleteCable, tooltipFields, refreshTrigger, + onViewTicket, + onCreateTicket, }) { const [activeTab, setActiveTab] = useState('ports'); + const [tickets, setTickets] = useState([]); + const [ticketsLoading, setTicketsLoading] = useState(false); + const [ticketsPagination, setTicketsPagination] = useState({ + current: 1, + pageSize: 5, + total: 0, + }); + + // 获取设备关联的工单列表 + const fetchDeviceTickets = useCallback(async (page = 1, pageSize = 5) => { + if (!device?.deviceId) return; + setTicketsLoading(true); + try { + const response = await deviceAPI.getTickets(device.deviceId, { + page, + pageSize, + }); + setTickets(response.data || []); + setTicketsPagination({ + current: response.page || 1, + pageSize: response.pageSize || 5, + total: response.total || 0, + }); + } catch (error) { + console.error('获取设备工单失败:', error); + } finally { + setTicketsLoading(false); + } + }, [device?.deviceId]); + + // 当设备变化或标签页切换到工单时,加载工单数据 + useEffect(() => { + if (visible && device?.deviceId && activeTab === 'tickets') { + fetchDeviceTickets(1, 5); + } + }, [visible, device?.deviceId, activeTab, fetchDeviceTickets]); + + // 工单表格列定义 + const ticketColumns = useMemo(() => [ + { + title: '工单编号', + dataIndex: 'ticketId', + key: 'ticketId', + width: 120, + render: (text) => {text}, + }, + { + title: '标题', + dataIndex: 'title', + key: 'title', + ellipsis: true, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 90, + render: (status) => { + const statusConfig = { + pending: { color: 'warning', text: '待处理' }, + processing: { color: 'processing', text: '处理中' }, + completed: { color: 'success', text: '已完成' }, + closed: { color: 'default', text: '已关闭' }, + }; + const config = statusConfig[status] || { color: 'default', text: status }; + return ; + }, + }, + { + title: '优先级', + dataIndex: 'priority', + key: 'priority', + width: 80, + render: (priority) => { + const priorityConfig = { + low: { color: 'success', text: '低' }, + medium: { color: 'warning', text: '中' }, + high: { color: 'error', text: '高' }, + critical: { color: 'purple', text: '紧急' }, + }; + const config = priorityConfig[priority] || { color: 'default', text: priority }; + return {config.text}; + }, + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + width: 150, + render: (date) => { + if (!date) return '-'; + const d = new Date(date); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; + }, + }, + { + title: '操作', + key: 'action', + width: 80, + render: (_, record) => ( + + ), + }, + ], [onViewTicket]); const deviceCables = useMemo(() => { if (!device || !cables) return []; @@ -249,6 +368,39 @@ function DeviceDetailDrawer({ ), }, + { + key: 'tickets', + label: ( + + + 工单记录 ({ticketsPagination.total}) + + ), + children: ( +
+
+ +
+ fetchDeviceTickets(page, pageSize), + }} + size="small" + /> + + ), + }, ]; if (!device) return null; diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 78f5c19..0a366a7 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -40,6 +40,7 @@ import { } from '@ant-design/icons'; import axios from 'axios'; import dayjs from 'dayjs'; +import { useNavigate } from 'react-router-dom'; import { designTokens } from '../config/theme'; import { PAGINATION_CONFIG, @@ -221,6 +222,7 @@ const ResizableTitle = props => { }; function DeviceManagement() { + const navigate = useNavigate(); const [devices, setDevices] = useState([]); const [allDevices, setAllDevices] = useState([]); const [racks, setRacks] = useState([]); @@ -756,6 +758,16 @@ function DeviceManagement() { setDetailModalVisible(true); }; + // 查看设备关联的工单 + const handleViewDeviceTickets = device => { + navigate(`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&view=true`); + }; + + // 为设备创建工单 + const handleCreateTicketForDevice = device => { + navigate(`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&create=true`); + }; + // 打开批量状态变更模态框 const showBatchStatusModal = () => { if (selectedDevices.length === 0) { @@ -2163,6 +2175,24 @@ function DeviceManagement() { > 关闭 , + , + ,