feat(工单): 支持手动输入设备信息或从列表选择

添加设备来源选项,允许用户在创建工单时选择从设备列表选择或手动输入设备信息
后端接口适配处理两种设备来源方式
添加terser依赖用于前端代码压缩优化
This commit is contained in:
zhang1106
2025-12-26 16:28:15 +08:00
parent e1e78f83c9
commit 7e0bf42b03
4 changed files with 159 additions and 20 deletions
+35 -12
View File
@@ -269,6 +269,8 @@ router.post('/', async (req, res) => {
try {
const {
deviceId,
deviceName,
serialNumber,
faultCategory,
faultSubCategory,
priority,
@@ -279,23 +281,44 @@ router.post('/', async (req, res) => {
tags
} = req.body;
// 获取设备信息
const device = await Device.findByPk(deviceId, {
include: [{ model: require('../models/Rack') }]
});
if (!device) {
return res.status(404).json({ error: '设备不存在' });
let device = null;
let ticketDeviceName = '';
let ticketDeviceModel = '';
let ticketSerialNumber = '';
let ticketLocation = '';
// 判断是选择设备还是手动输入
if (deviceId) {
// 从设备列表选择
device = await Device.findByPk(deviceId, {
include: [{ model: require('../models/Rack') }]
});
if (!device) {
return res.status(404).json({ error: '设备不存在' });
}
ticketDeviceName = device.name;
ticketDeviceModel = device.model;
ticketSerialNumber = device.serialNumber;
ticketLocation = device.Rack ? `${device.Rack.name}` : '未知位置';
} else if (deviceName && serialNumber) {
// 手动输入设备信息
ticketDeviceName = deviceName;
ticketSerialNumber = serialNumber;
ticketDeviceModel = '';
ticketLocation = '未知位置';
} else {
return res.status(400).json({ error: '请选择设备或手动输入设备信息' });
}
const ticketId = `TKT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
const ticket = await Ticket.create({
ticketId,
title: title || `${faultCategory} - ${device.name}`,
deviceId,
deviceName: device.name,
deviceModel: device.model,
serialNumber: device.serialNumber,
title: title || `${faultCategory} - ${ticketDeviceName}`,
deviceId: deviceId || null,
deviceName: ticketDeviceName,
deviceModel: ticketDeviceModel,
serialNumber: ticketSerialNumber,
faultCategory,
faultSubCategory,
priority: priority || 'medium',
@@ -303,7 +326,7 @@ router.post('/', async (req, res) => {
expectedCompletionDate: expectedCompletionDate ? new Date(expectedCompletionDate) : null,
reporterId: req.body.reporterId || 'USER001',
reporterName: req.body.reporterName || '系统用户',
location: device.Rack ? `${device.Rack.name}` : '未知位置',
location: ticketLocation,
attachments: attachments || [],
tags: tags || [],
status: 'pending'
+80
View File
@@ -24,6 +24,7 @@
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^4.0.3",
"jsdom": "^27.3.0",
"terser": "^5.44.1",
"vite": "^4.4.9",
"vitest": "^4.0.16"
}
@@ -1126,6 +1127,17 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -1908,6 +1920,19 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
@@ -2127,6 +2152,13 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -2220,6 +2252,13 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/compute-scroll-into-view": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
@@ -3890,6 +3929,16 @@
"dev": true,
"license": "ISC"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3900,6 +3949,17 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
@@ -3958,6 +4018,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/terser": {
"version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/three": {
"version": "0.160.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz",
+1
View File
@@ -25,6 +25,7 @@
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^4.0.3",
"jsdom": "^27.3.0",
"terser": "^5.44.1",
"vite": "^4.4.9",
"vitest": "^4.0.16"
}
+43 -8
View File
@@ -74,6 +74,7 @@ function TicketManagement() {
});
const [searchFilters, setSearchFilters] = useState({});
const [deviceSource, setDeviceSource] = useState('select');
const fetchTickets = useCallback(async (page = 1, pageSize = 10, filters = {}) => {
try {
@@ -148,9 +149,18 @@ function TicketManagement() {
if (ticketData.completionDate) {
ticketData.completionDate = dayjs(ticketData.completionDate);
}
if (ticket.deviceId) {
setDeviceSource('select');
ticketData.deviceId = ticket.deviceId;
} else {
setDeviceSource('manual');
ticketData.deviceName = ticket.deviceName;
ticketData.serialNumber = ticket.serialNumber;
}
form.setFieldsValue(ticketData);
} else {
form.resetFields();
setDeviceSource('select');
}
setModalVisible(true);
}, []);
@@ -168,6 +178,12 @@ function TicketManagement() {
completionDate: values.completionDate ? values.completionDate.format('YYYY-MM-DD HH:mm:ss') : null
};
if (deviceSource === 'manual') {
ticketData.deviceId = null;
ticketData.deviceName = values.deviceName;
ticketData.serialNumber = values.serialNumber;
}
if (editingTicket) {
await axios.put(`/api/tickets/${editingTicket.ticketId}`, ticketData);
message.success('工单更新成功');
@@ -182,11 +198,12 @@ function TicketManagement() {
setModalVisible(false);
fetchTickets();
setEditingTicket(null);
setDeviceSource('select');
} catch (error) {
message.error(editingTicket ? '工单更新失败' : '工单创建失败');
console.error(error);
}
}, [editingTicket, fetchTickets]);
}, [editingTicket, fetchTickets, deviceSource]);
const handleDelete = useCallback(async (ticketId) => {
Modal.confirm({
@@ -473,16 +490,34 @@ function TicketManagement() {
<Input placeholder="请输入工单标题" />
</Form.Item>
<Form.Item name="deviceId" label="关联设备" rules={[{ required: true }]}>
<Select placeholder="选择设备" showSearch optionFilterProp="children">
{devices.map(device => (
<Option key={device.deviceId} value={device.deviceId}>
{device.name} - {device.serialNumber}
</Option>
))}
<Form.Item label="设备来源">
<Select value={deviceSource} onChange={setDeviceSource} style={{ width: 200 }}>
<Option value="select">从设备列表选择</Option>
<Option value="manual">手动输入设备信息</Option>
</Select>
</Form.Item>
{deviceSource === 'select' ? (
<Form.Item name="deviceId" label="关联设备" rules={[{ required: true }]}>
<Select placeholder="选择设备" showSearch optionFilterProp="children">
{devices.map(device => (
<Option key={device.deviceId} value={device.deviceId}>
{device.name} - {device.serialNumber}
</Option>
))}
</Select>
</Form.Item>
) : (
<>
<Form.Item name="deviceName" label="设备名称" rules={[{ required: true }]}>
<Input placeholder="请输入设备名称" />
</Form.Item>
<Form.Item name="serialNumber" label="设备序列号" rules={[{ required: true }]}>
<Input placeholder="请输入设备序列号" />
</Form.Item>
</>
)}
<Form.Item name="faultCategory" label="故障分类" rules={[{ required: true }]}>
<Select placeholder="选择故障分类">
{categories.map(cat => (