feat(设备管理): 添加设备工单关联功能
实现设备与工单的关联功能,包括: 1. 后端添加设备工单列表接口 2. 前端API添加设备工单相关方法 3. 设备详情页增加工单标签页 4. 设备管理页添加查看/创建工单按钮 5. 工单管理页支持从设备跳转查看/创建工单
This commit is contained in:
@@ -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) => {
|
router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -125,6 +125,15 @@ export const operationLogAPI = {
|
|||||||
clear: data => api.delete('/operation-logs', { data }),
|
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 = {
|
export const ticketAPI = {
|
||||||
list: params => api.get('/tickets', { params }),
|
list: params => api.get('/tickets', { params }),
|
||||||
get: ticketId => api.get(`/tickets/${ticketId}`),
|
get: ticketId => api.get(`/tickets/${ticketId}`),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback, useMemo } from 'react';
|
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Button,
|
Button,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
|
Table,
|
||||||
|
Badge,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
@@ -18,8 +20,12 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
PlusCircleOutlined,
|
PlusCircleOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
EyeOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import NetworkCardPanel from './NetworkCardPanel';
|
import NetworkCardPanel from './NetworkCardPanel';
|
||||||
|
import { deviceAPI } from '../api';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
@@ -49,8 +55,121 @@ function DeviceDetailDrawer({
|
|||||||
onDeleteCable,
|
onDeleteCable,
|
||||||
tooltipFields,
|
tooltipFields,
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
|
onViewTicket,
|
||||||
|
onCreateTicket,
|
||||||
}) {
|
}) {
|
||||||
const [activeTab, setActiveTab] = useState('ports');
|
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 code>{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 <Badge status={config.color} text={config.text} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => onViewTicket?.(record)}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [onViewTicket]);
|
||||||
|
|
||||||
const deviceCables = useMemo(() => {
|
const deviceCables = useMemo(() => {
|
||||||
if (!device || !cables) return [];
|
if (!device || !cables) return [];
|
||||||
@@ -249,6 +368,39 @@ function DeviceDetailDrawer({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'tickets',
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<FileTextOutlined />
|
||||||
|
工单记录 ({ticketsPagination.total})
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div className="tickets-panel">
|
||||||
|
<div style={{ marginBottom: designTokens.spacing.md }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ToolOutlined />}
|
||||||
|
onClick={() => onCreateTicket?.(device)}
|
||||||
|
>
|
||||||
|
创建工单
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
columns={ticketColumns}
|
||||||
|
dataSource={tickets}
|
||||||
|
rowKey="ticketId"
|
||||||
|
loading={ticketsLoading}
|
||||||
|
pagination={{
|
||||||
|
...ticketsPagination,
|
||||||
|
onChange: (page, pageSize) => fetchDeviceTickets(page, pageSize),
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!device) return null;
|
if (!device) return null;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { designTokens } from '../config/theme';
|
import { designTokens } from '../config/theme';
|
||||||
import {
|
import {
|
||||||
PAGINATION_CONFIG,
|
PAGINATION_CONFIG,
|
||||||
@@ -221,6 +222,7 @@ const ResizableTitle = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function DeviceManagement() {
|
function DeviceManagement() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [allDevices, setAllDevices] = useState([]);
|
const [allDevices, setAllDevices] = useState([]);
|
||||||
const [racks, setRacks] = useState([]);
|
const [racks, setRacks] = useState([]);
|
||||||
@@ -756,6 +758,16 @@ function DeviceManagement() {
|
|||||||
setDetailModalVisible(true);
|
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 = () => {
|
const showBatchStatusModal = () => {
|
||||||
if (selectedDevices.length === 0) {
|
if (selectedDevices.length === 0) {
|
||||||
@@ -2163,6 +2175,24 @@ function DeviceManagement() {
|
|||||||
>
|
>
|
||||||
关闭
|
关闭
|
||||||
</Button>,
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="viewTickets"
|
||||||
|
onClick={() => {
|
||||||
|
handleViewDeviceTickets(selectedDevice);
|
||||||
|
}}
|
||||||
|
style={secondaryActionStyle}
|
||||||
|
>
|
||||||
|
查看工单
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="createTicket"
|
||||||
|
onClick={() => {
|
||||||
|
handleCreateTicketForDevice(selectedDevice);
|
||||||
|
}}
|
||||||
|
style={secondaryActionStyle}
|
||||||
|
>
|
||||||
|
创建工单
|
||||||
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key="edit"
|
key="edit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ import {
|
|||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
EnvironmentOutlined,
|
||||||
|
TagOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
// 安全地从 localStorage 获取用户信息
|
// 安全地从 localStorage 获取用户信息
|
||||||
const getUserFromStorage = () => {
|
const getUserFromStorage = () => {
|
||||||
@@ -225,6 +230,7 @@ const generateTicketId = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function TicketManagement() {
|
function TicketManagement() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [tickets, setTickets] = useState([]);
|
const [tickets, setTickets] = useState([]);
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
@@ -239,6 +245,13 @@ function TicketManagement() {
|
|||||||
const [processForm] = Form.useForm();
|
const [processForm] = Form.useForm();
|
||||||
const [searchForm] = Form.useForm();
|
const [searchForm] = Form.useForm();
|
||||||
|
|
||||||
|
// 从URL参数获取设备信息(从设备详情页跳转过来)
|
||||||
|
const urlDeviceId = searchParams.get('deviceId');
|
||||||
|
const urlDeviceName = searchParams.get('deviceName');
|
||||||
|
const urlSerialNumber = searchParams.get('serialNumber');
|
||||||
|
const urlCreate = searchParams.get('create');
|
||||||
|
const urlView = searchParams.get('view');
|
||||||
|
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -291,7 +304,7 @@ function TicketManagement() {
|
|||||||
|
|
||||||
const fetchDevices = useCallback(async () => {
|
const fetchDevices = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/devices', { params: { pageSize: 1000 } });
|
const response = await axios.get('/api/devices', { params: { pageSize: 100 } });
|
||||||
setDevices(response.data.devices || []);
|
setDevices(response.data.devices || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取设备列表失败:', error);
|
console.error('获取设备列表失败:', error);
|
||||||
@@ -333,9 +346,46 @@ function TicketManagement() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
fetchDevices();
|
fetchDevices();
|
||||||
fetchCategories();
|
fetchTicketFields().then(() => {
|
||||||
fetchTicketFields();
|
// 在 ticketFields 加载完成后再加载分类数据
|
||||||
}, [fetchTickets, fetchDevices, fetchCategories, fetchTicketFields]);
|
fetchCategories();
|
||||||
|
});
|
||||||
|
}, [fetchTickets, fetchDevices, fetchTicketFields, fetchCategories]);
|
||||||
|
|
||||||
|
// 处理从设备详情页跳转过来创建工单的情况
|
||||||
|
useEffect(() => {
|
||||||
|
if (urlDeviceId && devices.length > 0 && urlCreate === 'true') {
|
||||||
|
// 自动打开创建工单弹窗
|
||||||
|
setEditingTicket(null);
|
||||||
|
setDeviceSource('select');
|
||||||
|
setModalVisible(true);
|
||||||
|
|
||||||
|
// 填充设备信息 - 使用 setTimeout 确保弹窗打开后再设置表单值
|
||||||
|
setTimeout(() => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
deviceId: urlDeviceId,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [urlDeviceId, urlCreate, devices, form]);
|
||||||
|
|
||||||
|
// 处理从设备详情页跳转过来查看工单的情况
|
||||||
|
useEffect(() => {
|
||||||
|
if (urlDeviceId && urlView === 'true') {
|
||||||
|
// 设置搜索筛选条件,只显示该设备的工单
|
||||||
|
const filters = { deviceId: urlDeviceId };
|
||||||
|
setSearchFilters(filters);
|
||||||
|
// 在搜索框中显示设备名称提示
|
||||||
|
searchForm.setFieldsValue({ keyword: urlDeviceName || '' });
|
||||||
|
}
|
||||||
|
}, [urlDeviceId, urlView, urlDeviceName, searchForm]);
|
||||||
|
|
||||||
|
// 当 searchFilters 变化时,重新获取工单列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (urlDeviceId && urlView === 'true' && searchFilters.deviceId === urlDeviceId) {
|
||||||
|
fetchTickets(1, pagination.pageSize);
|
||||||
|
}
|
||||||
|
}, [searchFilters, urlDeviceId, urlView]);
|
||||||
|
|
||||||
const renderFormItem = useCallback(
|
const renderFormItem = useCallback(
|
||||||
field => {
|
field => {
|
||||||
@@ -740,9 +790,35 @@ function TicketManagement() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查 ticketFields 是否包含 deviceId
|
||||||
|
const hasDeviceIdField = ticketFields.some(f => f.fieldName === 'deviceId');
|
||||||
|
const hasDeviceNameField = ticketFields.some(f => f.fieldName === 'deviceName');
|
||||||
|
|
||||||
ticketFields.forEach(field => {
|
ticketFields.forEach(field => {
|
||||||
if (field.fieldName === 'ticketId' || field.fieldName === 'deviceId') {
|
if (field.fieldName === 'ticketId') {
|
||||||
if (field.fieldName === 'deviceId') {
|
// 已处理
|
||||||
|
} else if (field.fieldName === 'deviceId') {
|
||||||
|
// 渲染设备选择器
|
||||||
|
items.push(
|
||||||
|
<React.Fragment key="deviceSource">
|
||||||
|
<Form.Item label="设备来源" required>
|
||||||
|
<Select
|
||||||
|
value={deviceSource}
|
||||||
|
onChange={handleDeviceSourceChange}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
>
|
||||||
|
<Option value="select">从设备管理选择</Option>
|
||||||
|
<Option value="manual">手动输入</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{renderDeviceFormItems()}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else if (field.fieldName === 'deviceName' || field.fieldName === 'serialNumber') {
|
||||||
|
// 如果存在 deviceId 字段,这些字段会在 renderDeviceFormItems 中处理
|
||||||
|
// 如果不存在 deviceId 字段但存在 deviceName,则显示手动输入模式
|
||||||
|
if (!hasDeviceIdField && field.fieldName === 'deviceName') {
|
||||||
items.push(
|
items.push(
|
||||||
<React.Fragment key="deviceSource">
|
<React.Fragment key="deviceSource">
|
||||||
<Form.Item label="设备来源" required>
|
<Form.Item label="设备来源" required>
|
||||||
@@ -763,6 +839,26 @@ function TicketManagement() {
|
|||||||
items.push(renderFormItem(field));
|
items.push(renderFormItem(field));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 如果 ticketFields 中既没有 deviceId 也没有 deviceName,则手动添加设备选择器
|
||||||
|
if (!hasDeviceIdField && !hasDeviceNameField) {
|
||||||
|
items.push(
|
||||||
|
<React.Fragment key="deviceSource">
|
||||||
|
<Form.Item label="设备来源" required>
|
||||||
|
<Select
|
||||||
|
value={deviceSource}
|
||||||
|
onChange={handleDeviceSourceChange}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
>
|
||||||
|
<Option value="select">从设备管理选择</Option>
|
||||||
|
<Option value="manual">手动输入</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{renderDeviceFormItems()}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [
|
}, [
|
||||||
ticketFields,
|
ticketFields,
|
||||||
@@ -993,7 +1089,14 @@ function TicketManagement() {
|
|||||||
>
|
>
|
||||||
{selectedTicket && (
|
{selectedTicket && (
|
||||||
<Tabs defaultActiveKey="info">
|
<Tabs defaultActiveKey="info">
|
||||||
<TabPane tab="基本信息" key="info">
|
<TabPane
|
||||||
|
tab={
|
||||||
|
<span>
|
||||||
|
<TagOutlined /> 基本信息
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
key="info"
|
||||||
|
>
|
||||||
<Descriptions bordered column={2}>
|
<Descriptions bordered column={2}>
|
||||||
<Descriptions.Item label="工单编号">{selectedTicket.ticketId}</Descriptions.Item>
|
<Descriptions.Item label="工单编号">{selectedTicket.ticketId}</Descriptions.Item>
|
||||||
<Descriptions.Item label="标题">{selectedTicket.title}</Descriptions.Item>
|
<Descriptions.Item label="标题">{selectedTicket.title}</Descriptions.Item>
|
||||||
@@ -1042,7 +1145,94 @@ function TicketManagement() {
|
|||||||
</Descriptions>
|
</Descriptions>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane tab={`操作记录 (${operationRecords.length})`} key="operations">
|
{selectedTicket?.Device && (
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
|
<span>
|
||||||
|
<CloudServerOutlined /> 设备信息
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
key="device"
|
||||||
|
>
|
||||||
|
<Descriptions bordered column={2}>
|
||||||
|
<Descriptions.Item label="设备ID">
|
||||||
|
{selectedTicket.Device.deviceId}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="设备名称">
|
||||||
|
{selectedTicket.Device.name}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="设备类型">
|
||||||
|
{selectedTicket.Device.type}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="型号">
|
||||||
|
{selectedTicket.Device.model || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="序列号">
|
||||||
|
{selectedTicket.Device.serialNumber || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="品牌">
|
||||||
|
{selectedTicket.Device.brand || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="所在机房">
|
||||||
|
{selectedTicket.Device.roomName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="所在机柜">
|
||||||
|
{selectedTicket.Device.rackName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="位置(U)">
|
||||||
|
{selectedTicket.Device.position || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="高度(U)">
|
||||||
|
{selectedTicket.Device.height || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="IP地址">
|
||||||
|
{selectedTicket.Device.ipAddress || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="状态">
|
||||||
|
<Tag
|
||||||
|
color={
|
||||||
|
selectedTicket.Device.status === 'running'
|
||||||
|
? 'green'
|
||||||
|
: selectedTicket.Device.status === 'maintenance'
|
||||||
|
? 'orange'
|
||||||
|
: selectedTicket.Device.status === 'fault'
|
||||||
|
? 'red'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedTicket.Device.status === 'running'
|
||||||
|
? '运行中'
|
||||||
|
: selectedTicket.Device.status === 'maintenance'
|
||||||
|
? '维护中'
|
||||||
|
: selectedTicket.Device.status === 'fault'
|
||||||
|
? '故障'
|
||||||
|
: selectedTicket.Device.status === 'offline'
|
||||||
|
? '离线'
|
||||||
|
: selectedTicket.Device.status || '-'}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="购买日期">
|
||||||
|
{selectedTicket.Device.purchaseDate
|
||||||
|
? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD')
|
||||||
|
: '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="保修到期">
|
||||||
|
{selectedTicket.Device.warrantyDate
|
||||||
|
? dayjs(selectedTicket.Device.warrantyDate).format('YYYY-MM-DD')
|
||||||
|
: '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</TabPane>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
|
<span>
|
||||||
|
<ClockCircleOutlined /> 操作记录 ({operationRecords.length})
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
key="operations"
|
||||||
|
>
|
||||||
<Timeline mode="left">
|
<Timeline mode="left">
|
||||||
{operationRecords.map((record, index) => (
|
{operationRecords.map((record, index) => (
|
||||||
<Timeline.Item
|
<Timeline.Item
|
||||||
|
|||||||
Reference in New Issue
Block a user