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) => (
+ }
+ onClick={() => onViewTicket?.(record)}
+ >
+ 查看
+
+ ),
+ },
+ ], [onViewTicket]);
const deviceCables = useMemo(() => {
if (!device || !cables) return [];
@@ -249,6 +368,39 @@ function DeviceDetailDrawer({
),
},
+ {
+ key: 'tickets',
+ label: (
+
+
+ 工单记录 ({ticketsPagination.total})
+
+ ),
+ children: (
+
+
+ }
+ onClick={() => onCreateTicket?.(device)}
+ >
+ 创建工单
+
+
+
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() {
>
关闭
,
+ ,
+ ,