feat(工单/设备): 优化设备详情展示和工单操作记录样式

refactor(系统设置): 重构系统设置页面布局和样式

style(前端): 统一设备管理相关组件的卡片样式和布局
This commit is contained in:
zhang1106
2026-02-12 14:36:33 +08:00
parent 0b00bf6f3d
commit 8de7096808
6 changed files with 1459 additions and 870 deletions
+25 -2
View File
@@ -5,6 +5,8 @@ const { v4: uuidv4 } = require('uuid');
const { Ticket, TicketOperationRecord } = require('../models/ticketIndex'); const { Ticket, TicketOperationRecord } = require('../models/ticketIndex');
const Device = require('../models/Device'); const Device = require('../models/Device');
const User = require('../models/User'); const User = require('../models/User');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const { dbDialect } = require('../db'); const { dbDialect } = require('../db');
// 获取工单统计 (必须定义在 /:ticketId 之前) // 获取工单统计 (必须定义在 /:ticketId 之前)
@@ -238,7 +240,21 @@ router.get('/:ticketId', async (req, res) => {
const ticket = await Ticket.findByPk(req.params.ticketId, { const ticket = await Ticket.findByPk(req.params.ticketId, {
include: [ include: [
{ model: User, as: 'reporter', attributes: ['userId', 'username', 'email'] }, { model: User, as: 'reporter', attributes: ['userId', 'username', 'email'] },
{ model: Device }, {
model: Device,
include: [
{
model: Rack,
attributes: ['rackId', 'name', 'roomId'],
include: [
{
model: Room,
attributes: ['roomId', 'name']
}
]
}
]
},
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] } { model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] }
] ]
}); });
@@ -247,7 +263,14 @@ router.get('/:ticketId', async (req, res) => {
return res.status(404).json({ error: '工单不存在' }); return res.status(404).json({ error: '工单不存在' });
} }
res.json(ticket); // 格式化响应数据,添加机房和机柜名称
const ticketData = ticket.toJSON();
if (ticketData.Device && ticketData.Device.Rack) {
ticketData.Device.roomName = ticketData.Device.Rack.Room?.name || '-';
ticketData.Device.rackName = ticketData.Device.Rack.name || '-';
}
res.json(ticketData);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
+180 -158
View File
@@ -12,6 +12,9 @@ import {
Popconfirm, Popconfirm,
Table, Table,
Badge, Badge,
Row,
Col,
Divider,
} from 'antd'; } from 'antd';
import { import {
ApiOutlined, ApiOutlined,
@@ -23,22 +26,29 @@ import {
FileTextOutlined, FileTextOutlined,
ToolOutlined, ToolOutlined,
EyeOutlined, EyeOutlined,
DesktopOutlined,
FieldTimeOutlined,
InfoCircleOutlined,
LinkOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import NetworkCardPanel from './NetworkCardPanel'; import NetworkCardPanel from './NetworkCardPanel';
import { deviceAPI } from '../api'; import { deviceAPI } from '../api';
import dayjs from 'dayjs';
const { Text, Title } = Typography; const { Text, Title } = Typography;
const designTokens = { const designTokens = {
colors: { colors: {
primary: '#667eea', primary: '#1890ff',
success: '#10b981', success: '#52c41a',
error: '#ef4444', error: '#f5222d',
warning: '#f59e0b', warning: '#faad14',
}, info: '#13c2c2',
spacing: { gray: '#8c8c8c',
sm: 8, bgLight: '#f6ffed',
md: 16, bgBlue: '#e6f7ff',
bgGray: '#f5f5f5',
bgOrange: '#fff7e6',
}, },
}; };
@@ -150,8 +160,7 @@ function DeviceDetailDrawer({
width: 150, width: 150,
render: (date) => { render: (date) => {
if (!date) return '-'; if (!date) return '-';
const d = new Date(date); return dayjs(date).format('YYYY-MM-DD HH:mm');
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')}`;
}, },
}, },
{ {
@@ -205,49 +214,12 @@ function DeviceDetailDrawer({
return typeMap[type?.toLowerCase()] || type || '未知设备'; return typeMap[type?.toLowerCase()] || type || '未知设备';
}, []); }, []);
const renderFieldValue = useCallback( if (!device) return null;
(field, device) => {
const fieldKey = field.field;
if (fieldKey === 'status') return getStatusTag(device.status);
// 优先从device对象获取值,如果没有则从customFields中获取 // 解析自定义字段
let value = device[fieldKey]; const customFields = device.customFields || {};
if ( const standardFields = ['deviceId', 'name', 'type', 'model', 'serialNumber', 'status', 'ipAddress', 'position', 'height', 'powerConsumption', 'purchaseDate', 'warrantyExpiry', 'description'];
(value === undefined || value === null) && const customFieldEntries = Object.entries(customFields).filter(([key]) => !standardFields.includes(key));
device.customFields &&
typeof device.customFields === 'object'
) {
value = device.customFields[fieldKey];
}
if (fieldKey === 'type') value = getDeviceTypeName(value);
else if (fieldKey === 'position')
value = `U${device.position} ${device.height ? `(${device.height}U)` : ''}`;
return (
<Text strong style={{ fontSize: '14px' }}>
{value !== undefined && value !== null ? value : '-'}
</Text>
);
},
[getStatusTag, getDeviceTypeName]
);
const displayFields = useMemo(() => {
if (tooltipFields && Object.keys(tooltipFields).length > 0) {
return Object.values(tooltipFields).filter(f => f.enabled);
}
// Default fallback fields if no config
return [
{ field: 'deviceId', label: '设备ID' },
{ field: 'type', label: '设备类型' },
{ field: 'status', label: '设备状态' },
{ field: 'position', label: '位置' },
{ field: 'ipAddress', label: 'IP地址' },
{ field: 'brand', label: '品牌' },
];
}, [tooltipFields]);
const tabItems = [ const tabItems = [
{ {
@@ -271,7 +243,7 @@ function DeviceDetailDrawer({
key: 'cables', key: 'cables',
label: ( label: (
<span> <span>
<EnvironmentOutlined /> <LinkOutlined />
接线 ({deviceCables.length}) 接线 ({deviceCables.length})
</span> </span>
), ),
@@ -280,7 +252,7 @@ function DeviceDetailDrawer({
{deviceCables.length === 0 ? ( {deviceCables.length === 0 ? (
<Empty description="该设备暂无接线" /> <Empty description="该设备暂无接线" />
) : ( ) : (
<Space direction="vertical" size={designTokens.spacing.md} style={{ width: '100%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
{deviceCables.map(cable => ( {deviceCables.map(cable => (
<Card <Card
key={cable.cableId} key={cable.cableId}
@@ -297,70 +269,60 @@ function DeviceDetailDrawer({
</Popconfirm> </Popconfirm>
} }
> >
<div style={{ marginBottom: designTokens.spacing.sm }}> <Row gutter={[16, 8]}>
<Space direction="vertical" size={4}> <Col span={12}>
<div> <div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>源设备</div>
<Text type="secondary" style={{ fontSize: '12px' }}> <div style={{ fontWeight: 500 }}>
源设备 {cable.sourceDevice?.name || '-'}
</Text> <Tag color="blue" style={{ marginLeft: '8px' }}>
<div style={{ fontWeight: 500 }}> {cable.sourcePort}
{cable.sourceDevice?.name || '-'} </Tag>
<Tag color="blue" style={{ marginLeft: '8px' }}>
{cable.sourcePort}
</Tag>
</div>
</div> </div>
<div> </Col>
<Text type="secondary" style={{ fontSize: '12px' }}> <Col span={12}>
目标设备 <div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>目标设备</div>
</Text> <div style={{ fontWeight: 500 }}>
<div style={{ fontWeight: 500 }}> {cable.targetDevice?.name || '-'}
{cable.targetDevice?.name || '-'} <Tag color="green" style={{ marginLeft: '8px' }}>
<Tag color="green" style={{ marginLeft: '8px' }}> {cable.targetPort}
{cable.targetPort} </Tag>
</Tag>
</div>
</div> </div>
</Space> </Col>
</div> <Col span={24}>
<Space wrap style={{ marginTop: 8 }}>
<Space wrap> <Tag
<Tag color={
color={ cable.status === 'normal'
cable.status === 'normal' ? 'success'
? 'success' : cable.status === 'fault'
: cable.status === 'fault' ? 'error'
? 'error' : 'default'
: 'default' }
} >
> {cable.status === 'normal'
{cable.status === 'normal' ? '正常'
? '正常' : cable.status === 'fault'
: cable.status === 'fault' ? '故障'
? '故障' : '未连接'}
: '未连接'} </Tag>
</Tag> <Tag color="purple">
<Tag color="purple"> {cable.cableType === 'ethernet'
{cable.cableType === 'ethernet' ? '网线'
? '网线' : cable.cableType === 'fiber'
: cable.cableType === 'fiber' ? '光纤'
? '光纤' : '铜缆'}
: '铜缆'} </Tag>
</Tag> {cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>} </Space>
</Space> </Col>
{cable.description && (
{cable.description && ( <Col span={24}>
<div <div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
style={{ {cable.description}
marginTop: designTokens.spacing.sm, </div>
fontSize: '12px', </Col>
color: '#666', )}
}} </Row>
>
{cable.description}
</div>
)}
</Card> </Card>
))} ))}
</Space> </Space>
@@ -378,7 +340,7 @@ function DeviceDetailDrawer({
), ),
children: ( children: (
<div className="tickets-panel"> <div className="tickets-panel">
<div style={{ marginBottom: designTokens.spacing.md }}> <div style={{ marginBottom: 16 }}>
<Button <Button
type="primary" type="primary"
icon={<ToolOutlined />} icon={<ToolOutlined />}
@@ -403,32 +365,24 @@ function DeviceDetailDrawer({
}, },
]; ];
if (!device) return null;
return ( return (
<Drawer <Drawer
title={ title={
<Space style={{ maxWidth: '280px', overflow: 'hidden' }}> <Space>
<CloudServerOutlined style={{ color: designTokens.colors.primary, flexShrink: 0 }} /> <CloudServerOutlined style={{ color: designTokens.colors.primary, fontSize: 20 }} />
<span <span style={{ fontSize: 18, fontWeight: 600 }}>设备详情</span>
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
设备详情 - {device.name}
</span>
</Space> </Space>
} }
placement="right" placement="right"
width={600} width={700}
open={visible} open={visible}
onClose={onClose} onClose={onClose}
extra={ extra={
<Space> <Space>
<Tooltip title="编辑设备信息"> <Tooltip title="编辑设备信息">
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)} /> <Button icon={<EditOutlined />} onClick={() => onEdit?.(device)}>
编辑
</Button>
</Tooltip> </Tooltip>
<Tooltip title="添加网卡"> <Tooltip title="添加网卡">
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}> <Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>
@@ -447,35 +401,103 @@ function DeviceDetailDrawer({
</Tooltip> </Tooltip>
</Space> </Space>
} }
styles={{ body: { padding: '16px 20px', overflow: 'auto' } }} styles={{ body: { padding: '0', overflow: 'auto' } }}
> >
<div className="device-info-section" style={{ marginBottom: '20px' }}> {/* 设备头部信息 */}
<Title level={5} style={{ margin: '0 0 12px 0', color: '#1e293b' }}> <div style={{ padding: '20px 24px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: '#fff' }}>
基本信息 <Row gutter={[16, 16]} align="middle">
</Title> <Col>
<div <DesktopOutlined style={{ fontSize: 48, opacity: 0.9 }} />
className="info-grid" </Col>
style={{ <Col flex="auto">
display: 'grid', <div style={{ fontSize: 24, fontWeight: 600, marginBottom: 8 }}>{device.name}</div>
gridTemplateColumns: 'repeat(2, 1fr)', <Space size={16}>
gap: '12px', <span>{getDeviceTypeName(device.type)}</span>
background: '#f8fafc', <span>|</span>
padding: '16px', <span>{device.deviceId}</span>
borderRadius: '10px', <span>|</span>
}} {getStatusTag(device.status)}
> </Space>
{displayFields.map(field => ( </Col>
<div className="info-item" key={field.field}> </Row>
<Text type="secondary" style={{ fontSize: '12px', display: 'block' }}>
{field.label}
</Text>
{renderFieldValue(field, device)}
</div>
))}
</div>
</div> </div>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} /> {/* 设备信息内容 */}
<div style={{ padding: '20px 24px' }}>
{/* 基本信息卡片 */}
<Card title="基本信息" size="small" style={{ marginBottom: 16 }}>
<Row gutter={[24, 16]}>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>设备型号</div>
<div style={{ fontWeight: 500 }}>{device.model || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>序列号</div>
<div style={{ fontWeight: 500 }}>{device.serialNumber || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>IP地址</div>
<div style={{ fontWeight: 500 }}>{device.ipAddress || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>位置</div>
<div style={{ fontWeight: 500 }}>U{device.position || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>高度</div>
<div style={{ fontWeight: 500 }}>{device.height ? `${device.height}U` : '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>功耗</div>
<div style={{ fontWeight: 500 }}>{device.powerConsumption ? `${device.powerConsumption}W` : '-'}</div>
</Col>
</Row>
</Card>
{/* 维保信息卡片 */}
<Card title="维保信息" size="small" style={{ marginBottom: 16 }}>
<Row gutter={[24, 16]}>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>购买日期</div>
<div style={{ fontWeight: 500 }}>
{device.purchaseDate ? dayjs(device.purchaseDate).format('YYYY-MM-DD') : '-'}
</div>
</Col>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>保修到期</div>
<div style={{ fontWeight: 500 }}>
{device.warrantyExpiry ? dayjs(device.warrantyExpiry).format('YYYY-MM-DD') : '-'}
</div>
</Col>
</Row>
</Card>
{/* 描述信息 */}
{device.description && (
<Card title="描述" size="small" style={{ marginBottom: 16 }}>
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>{device.description}</div>
</Card>
)}
{/* 自定义字段卡片 */}
{customFieldEntries.length > 0 && (
<Card title="自定义字段" size="small" style={{ marginBottom: 16 }}>
<Row gutter={[24, 16]}>
{customFieldEntries.map(([key, value]) => (
<Col span={8} key={key}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>{key}</div>
<div style={{ fontWeight: 500 }}>{String(value)}</div>
</Col>
))}
</Row>
</Card>
)}
<Divider style={{ margin: '24px 0' }} />
{/* 标签页 */}
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
</div>
</Drawer> </Drawer>
); );
} }
+155 -145
View File
@@ -18,6 +18,9 @@ import {
Spin, Spin,
Dropdown, Dropdown,
Tooltip, Tooltip,
Tag,
Row,
Col,
} from 'antd'; } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
@@ -2154,7 +2157,7 @@ function DeviceManagement() {
<Modal <Modal
title={ title={
<div style={modalHeaderStyle}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: 600 }}>
<AppstoreOutlined style={{ color: '#667eea' }} /> <AppstoreOutlined style={{ color: '#667eea' }} />
设备详情 设备详情
</div> </div>
@@ -2216,161 +2219,168 @@ function DeviceManagement() {
编辑 编辑
</Button>, </Button>,
]} ]}
width={800} width={700}
destroyOnHidden destroyOnHidden
styles={{ styles={{
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
body: { padding: '24px' }, body: { padding: '0', overflow: 'auto' },
}} }}
> >
{selectedDevice && ( {selectedDevice && (
<div> <div>
<Card {/* 头部信息区域 */}
size="small" <div style={{
title={ padding: '24px',
<span style={{ fontWeight: '600' }}> background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
<CloudServerOutlined style={{ marginRight: '8px', color: '#667eea' }} /> color: '#fff'
基本信息 }}>
</span> <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
} <div style={{
style={{ borderRadius: '12px', border: '1px solid #f0f0f0' }} width: '64px',
> height: '64px',
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px' }}> borderRadius: '12px',
<div> backgroundColor: 'rgba(255,255,255,0.2)',
<label style={{ fontWeight: '500', color: '#666' }}>设备ID</label> display: 'flex',
<span style={{ marginLeft: 8, color: '#333' }}> alignItems: 'center',
{selectedDevice.deviceId || '-'} justifyContent: 'center'
</span> }}>
{getDeviceTypeIcon(selectedDevice.type)}
</div> </div>
<div> <div style={{ flex: 1 }}>
<label style={{ fontWeight: '500', color: '#666' }}>设备名称</label> <div style={{ fontSize: '24px', fontWeight: 600, marginBottom: '8px' }}>
<span style={{ marginLeft: 8, color: '#333' }}>{selectedDevice.name || '-'}</span> {selectedDevice.name}
</div> </div>
<div> <div style={{ display: 'flex', alignItems: 'center', gap: '16px', opacity: 0.9 }}>
<label style={{ fontWeight: '500', color: '#666' }}>设备类型</label> <span>{getTypeLabel(selectedDevice.type)}</span>
<span style={{ marginLeft: 8 }}> <span>|</span>
{selectedDevice.type ? ( <span>{selectedDevice.deviceId}</span>
<Space> <span>|</span>
{getDeviceTypeIcon(selectedDevice.type)} <Tag color={selectedDevice.status ? getStatusConfig(selectedDevice.status).badgeColor : 'default'} style={{ margin: 0 }}>
<span>{getTypeLabel(selectedDevice.type)}</span> {selectedDevice.status ? getStatusConfig(selectedDevice.status).text : '-'}
</Space> </Tag>
) : (
'-'
)}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>型号</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.model || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>序列号</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.serialNumber || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>IP地址</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.ipAddress || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>所在机房</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.Rack?.Room?.name || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>所在机柜</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.Rack?.name || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>位置</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.position || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>高度(U)</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.height || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>功率(W)</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.power || '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>状态</label>
<span
style={{
marginLeft: 8,
color: selectedDevice.status
? getStatusConfig(selectedDevice.status).color
: '#666',
fontWeight: '600',
}}
>
{selectedDevice.status
? getStatusConfig(selectedDevice.status).text || selectedDevice.status
: '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>购买日期</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.purchaseDate
? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN')
: '-'}
</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>保修到期</label>
<span
style={{
marginLeft: 8,
color:
selectedDevice.warrantyExpiry &&
new Date(selectedDevice.warrantyExpiry) < new Date()
? '#d93025'
: '#333',
fontWeight:
selectedDevice.warrantyExpiry &&
new Date(selectedDevice.warrantyExpiry) < new Date()
? '600'
: 'normal',
}}
>
{selectedDevice.warrantyExpiry
? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN')
: '-'}
</span>
</div>
</div>
{selectedDevice.description && (
<div style={{ marginTop: '16px' }}>
<label style={{ fontWeight: '500', color: '#666' }}>描述</label>
<div
style={{
marginTop: '8px',
padding: '12px',
backgroundColor: '#fafafa',
borderRadius: '8px',
color: '#333',
}}
>
{selectedDevice.description}
</div> </div>
</div> </div>
</div>
</div>
{/* 内容区域 */}
<div style={{ padding: '20px 24px' }}>
{/* 基本信息卡片 */}
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>基本信息</span>}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
<Row gutter={[24, 16]}>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>设备型号</div>
<div style={{ fontWeight: 500 }}>{selectedDevice.model || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>序列号</div>
<div style={{ fontWeight: 500 }}>{selectedDevice.serialNumber || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>IP地址</div>
<div style={{ fontWeight: 500 }}>{selectedDevice.ipAddress || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>所在机房</div>
<div style={{ fontWeight: 500 }}>{selectedDevice.Rack?.Room?.name || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>所在机柜</div>
<div style={{ fontWeight: 500 }}>{selectedDevice.Rack?.name || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>位置(U)</div>
<div style={{ fontWeight: 500 }}>U{selectedDevice.position || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>高度</div>
<div style={{ fontWeight: 500 }}>{selectedDevice.height ? `${selectedDevice.height}U` : '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>功率</div>
<div style={{ fontWeight: 500 }}>{selectedDevice.power ? `${selectedDevice.power}W` : '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>状态</div>
<div style={{
fontWeight: 500,
color: selectedDevice.status ? getStatusConfig(selectedDevice.status).color : '#666'
}}>
{selectedDevice.status ? getStatusConfig(selectedDevice.status).text : '-'}
</div>
</Col>
</Row>
</Card>
{/* 维保信息卡片 */}
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>维保信息</span>}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
<Row gutter={[24, 16]}>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>购买日期</div>
<div style={{ fontWeight: 500 }}>
{selectedDevice.purchaseDate
? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN')
: '-'}
</div>
</Col>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>保修到期</div>
<div style={{
fontWeight: selectedDevice.warrantyExpiry && new Date(selectedDevice.warrantyExpiry) < new Date() ? 600 : 500,
color: selectedDevice.warrantyExpiry && new Date(selectedDevice.warrantyExpiry) < new Date() ? '#d93025' : '#333'
}}>
{selectedDevice.warrantyExpiry
? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN')
: '-'}
</div>
</Col>
</Row>
</Card>
{/* 描述信息 */}
{selectedDevice.description && (
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>描述</span>}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>
{selectedDevice.description}
</div>
</Card>
)} )}
</Card>
{/* 自定义字段卡片 */}
{selectedDevice.customFields && Object.keys(selectedDevice.customFields).length > 0 && (
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>自定义字段</span>}
style={{ borderRadius: '8px' }}
>
<Row gutter={[24, 16]}>
{Object.entries(selectedDevice.customFields).map(([key, value]) => {
// 从 deviceFields 中查找对应的中文显示名称
const fieldConfig = deviceFields.find(f => f.fieldName === key);
const displayName = fieldConfig?.displayName || key;
return (
<Col span={8} key={key}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>{displayName}</div>
<div style={{ fontWeight: 500 }}>{String(value)}</div>
</Col>
);
})}
</Row>
</Card>
)}
</div>
</div> </div>
)} )}
</Modal> </Modal>
+505 -324
View File
@@ -1,18 +1,31 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Card, message, Typography, Divider, Space, Alert } from 'antd'; import {
Form,
Input,
Button,
Card,
message,
Typography,
Divider,
Space,
Alert,
Row,
Col,
} from 'antd';
import { import {
UserOutlined, UserOutlined,
LockOutlined, LockOutlined,
MailOutlined, MailOutlined,
PhoneOutlined, PhoneOutlined,
SafetyCertificateOutlined, SafetyCertificateOutlined,
RobotOutlined, CloudServerOutlined,
ArrowLeftOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { authAPI } from '../api'; import { authAPI } from '../api';
const { Title, Text } = Typography; const { Title, Text, Paragraph } = Typography;
const Login = () => { const Login = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -115,359 +128,527 @@ const Login = () => {
} }
}; };
const containerStyle = { // 左侧宣传区域组件
minHeight: '100vh', const LeftPanel = () => (
display: 'flex', <div
justifyContent: 'center', style={{
alignItems: 'center', height: '100%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)', display: 'flex',
padding: '24px', flexDirection: 'column',
position: 'relative', justifyContent: 'center',
overflow: 'hidden', padding: '60px',
}; color: '#fff',
position: 'relative',
const backgroundDecorationStyle = { overflow: 'hidden',
position: 'absolute', }}
borderRadius: '50%', >
filter: 'blur(80px)', {/* 背景装饰 */}
opacity: '0.3',
};
const cardStyle = {
width: '100%',
maxWidth: isFirstUser ? 480 : 420,
borderRadius: '20px',
boxShadow: '0 20px 60px rgba(0,0,0,0.25), 0 8px 20px rgba(0,0,0,0.15)',
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
};
const headerStyle = {
textAlign: 'center',
marginBottom: '32px',
paddingTop: '8px',
};
const iconContainerStyle = {
width: '80px',
height: '80px',
borderRadius: '20px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 20px',
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.4)',
};
const titleStyle = {
fontSize: '26px',
fontWeight: '700',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '8px',
};
const subtitleStyle = {
fontSize: '14px',
color: '#8c8c8c',
};
const formStyle = {
marginTop: '24px',
};
const inputStyle = {
borderRadius: '8px',
height: '48px',
border: '1px solid #e8e8e8',
};
const submitButtonStyle = {
width: '100%',
height: '48px',
fontSize: '16px',
fontWeight: '600',
borderRadius: '8px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4)',
transition: 'all 0.3s ease',
};
const footerStyle = {
textAlign: 'center',
marginTop: '24px',
paddingBottom: '16px',
};
const toggleButtonStyle = {
color: '#667eea',
fontWeight: '500',
padding: '4px 8px',
borderRadius: '4px',
transition: 'all 0.3s ease',
};
const inputPrefixStyle = {
color: '#667eea',
fontSize: '18px',
};
return (
<div style={containerStyle}>
<div <div
style={{ style={{
...backgroundDecorationStyle, position: 'absolute',
width: '600px',
height: '600px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.1)',
top: '-200px',
left: '-200px',
filter: 'blur(60px)',
}}
/>
<div
style={{
position: 'absolute',
width: '400px', width: '400px',
height: '400px', height: '400px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', borderRadius: '50%',
top: '-100px', background: 'rgba(255,255,255,0.08)',
bottom: '-100px',
right: '-100px', right: '-100px',
}} filter: 'blur(40px)',
/>
<div
style={{
...backgroundDecorationStyle,
width: '300px',
height: '300px',
background: 'linear-gradient(135deg, #764ba2 0%, #6B8DD6 100%)',
bottom: '-50px',
left: '-50px',
}} }}
/> />
<Card style={cardStyle}> <div style={{ position: 'relative', zIndex: 1 }}>
<div style={headerStyle}> <div
<div style={iconContainerStyle}> style={{
<RobotOutlined style={{ fontSize: '40px', color: '#fff' }} /> width: '80px',
</div> height: '80px',
<Title level={2} style={titleStyle}> borderRadius: '20px',
{isFirstUser ? '创建管理员账户' : unlockMode ? '账户解锁' : 'IDC设备管理系统'} background: 'rgba(255,255,255,0.2)',
</Title> display: 'flex',
<Text style={subtitleStyle}> alignItems: 'center',
{isFirstUser justifyContent: 'center',
? '首次使用,请创建系统管理员账户' marginBottom: '40px',
: unlockMode backdropFilter: 'blur(10px)',
? '输入账户信息以解锁账户' border: '1px solid rgba(255,255,255,0.3)',
: '安全登录您的账户'} }}
</Text> >
<CloudServerOutlined style={{ fontSize: '40px', color: '#fff' }} />
</div> </div>
{isFirstUser && ( <Title
<Alert level={1}
message="欢迎使用IDC设备管理系统" style={{
description="您是第一个用户,系统将自动为您分配管理员权限。" color: '#fff',
type="success" fontSize: '48px',
showIcon fontWeight: 700,
style={{ marginBottom: '24px', borderRadius: '8px' }} marginBottom: '24px',
/> lineHeight: 1.2,
)} }}
<Form
name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'}
size="large"
onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin}
style={formStyle}
> >
{registerMode ? ( IDC设备
<> <br />
<Form.Item 管理系统
name="username" </Title>
rules={[
{ required: true, message: '请输入用户名' },
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
]}
>
<Input
prefix={<UserOutlined style={inputPrefixStyle} />}
placeholder="用户名"
style={inputStyle}
/>
</Form.Item>
<Form.Item name="realName" rules={[{ required: true, message: '请输入真实姓名' }]}> <Paragraph
<Input style={{
prefix={<SafetyCertificateOutlined style={inputPrefixStyle} />} color: 'rgba(255,255,255,0.85)',
placeholder="真实姓名" fontSize: '18px',
style={inputStyle} lineHeight: 1.8,
/> maxWidth: '480px',
</Form.Item> marginBottom: '48px',
}}
>
专业的数据中心设备管理平台提供机房机柜设备的全生命周期管理
助力企业实现高效的IT资产管理
</Paragraph>
<Form.Item <Row gutter={[24, 24]}>
name="email" <Col xs={8} sm={8} md={8}>
rules={[ <div style={{ textAlign: 'center' }}>
{ required: true, message: '请输入邮箱' }, <div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>99.9%</div>
{ type: 'email', message: '请输入有效的邮箱地址' }, <div style={{ fontSize: '13px', opacity: 0.8 }}>系统稳定性</div>
]} </div>
> </Col>
<Input <Col xs={8} sm={8} md={8}>
prefix={<MailOutlined style={inputPrefixStyle} />} <div style={{ textAlign: 'center' }}>
placeholder="邮箱" <div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>24/7</div>
style={inputStyle} <div style={{ fontSize: '13px', opacity: 0.8 }}>全天候监控</div>
/> </div>
</Form.Item> </Col>
<Col xs={8} sm={8} md={8}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>100%</div>
<div style={{ fontSize: '13px', opacity: 0.8 }}>数据安全</div>
</div>
</Col>
</Row>
</div>
</div>
);
<Form.Item name="phone"> // 获取标题和副标题
<Input const getHeaderContent = () => {
prefix={<PhoneOutlined style={inputPrefixStyle} />} if (isFirstUser) {
placeholder="手机号(可选)" return {
style={inputStyle} title: '创建管理员账户',
/> subtitle: '首次使用,请创建系统管理员账户',
</Form.Item> };
}
if (unlockMode) {
return {
title: '账户解锁',
subtitle: '输入账户信息以解锁账户',
};
}
if (registerMode) {
return {
title: '注册新账户',
subtitle: '填写信息完成账户注册',
};
}
return {
title: '欢迎回来',
subtitle: '请登录您的账户以继续',
};
};
<Form.Item const headerContent = getHeaderContent();
name="password"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码长度不能少于6个字符' },
]}
>
<Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />}
placeholder="密码"
style={inputStyle}
/>
</Form.Item>
<Form.Item return (
name="confirmPassword" <Row style={{ minHeight: '100vh', overflow: 'hidden' }}>
dependencies={['password']} {/* 左侧区域 - 桌面端显示 */}
rules={[ <Col
{ required: true, message: '请确认密码' }, xs={0}
({ getFieldValue }) => ({ sm={0}
validator(_, value) { md={0}
if (!value || getFieldValue('password') === value) { lg={12}
return Promise.resolve(); xl={14}
} style={{
return Promise.reject(new Error('两次输入的密码不一致')); background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)',
}, }}
}), >
]} <LeftPanel />
> </Col>
<Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />}
placeholder="确认密码"
style={inputStyle}
/>
</Form.Item>
</>
) : unlockMode ? (
<>
<Alert
message="账户解锁说明"
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
type="info"
showIcon
style={{ marginBottom: '24px', borderRadius: '8px' }}
/>
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}> {/* 右侧登录区域 */}
<Input <Col
prefix={<UserOutlined style={inputPrefixStyle} />} xs={24}
placeholder="用户名" sm={24}
style={inputStyle} md={24}
/> lg={12}
</Form.Item> xl={10}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f8fafc',
padding: '24px',
position: 'relative',
}}
>
{/* 移动端背景 */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '200px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'none',
}}
className="mobile-bg"
/>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}> <Card
<Input.Password style={{
prefix={<LockOutlined style={inputPrefixStyle} />} width: '100%',
placeholder="密码" maxWidth: registerMode ? 520 : 440,
style={inputStyle} borderRadius: '24px',
/> boxShadow: '0 25px 80px rgba(0,0,0,0.15), 0 10px 30px rgba(0,0,0,0.1)',
</Form.Item> background: '#fff',
</> border: 'none',
) : ( position: 'relative',
<> zIndex: 1,
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}> }}
<Input bodyStyle={{ padding: '48px' }}
prefix={<UserOutlined style={inputPrefixStyle} />} >
placeholder="用户名" {/* 返回按钮 */}
style={inputStyle} {(registerMode || unlockMode) && !isFirstUser && (
/> <Button
</Form.Item> type="link"
icon={<ArrowLeftOutlined />}
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}> onClick={() => {
<Input.Password setRegisterMode(false);
prefix={<LockOutlined style={inputPrefixStyle} />} setUnlockMode(false);
placeholder="密码" }}
style={inputStyle} style={{
/> position: 'absolute',
</Form.Item> top: '24px',
</> left: '24px',
color: '#667eea',
padding: '4px 8px',
}}
>
返回
</Button>
)} )}
<Form.Item style={{ marginBottom: '16px', marginTop: '24px' }}> {/* 头部 */}
<Button type="primary" htmlType="submit" loading={loading} style={submitButtonStyle}> <div style={{ textAlign: 'center', marginBottom: '32px' }}>
{registerMode ? '立即注册' : unlockMode ? '解 锁' : '登 录'} <div
</Button> style={{
</Form.Item> width: '64px',
</Form> height: '64px',
borderRadius: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 20px',
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.35)',
}}
>
<CloudServerOutlined style={{ fontSize: '32px', color: '#fff' }} />
</div>
<Title
level={3}
style={{
fontSize: '28px',
fontWeight: 700,
color: '#1e293b',
marginBottom: '8px',
}}
>
{headerContent.title}
</Title>
<Text style={{ fontSize: '15px', color: '#64748b' }}>
{headerContent.subtitle}
</Text>
</div>
{!isFirstUser && ( {/* 首次使用提示 */}
<div style={footerStyle}> {isFirstUser && (
<Divider plain> <Alert
<Text style={{ color: '#8c8c8c', fontSize: '12px' }}>其他方式</Text> message="欢迎使用IDC设备管理系统"
</Divider> description="您是第一个用户,系统将自动为您分配管理员权限。"
<Space split={<Divider type="vertical" />}> type="success"
{unlockMode ? ( showIcon
<> style={{ marginBottom: '24px', borderRadius: '12px' }}
/>
)}
{/* 解锁模式提示 */}
{unlockMode && (
<Alert
message="账户解锁说明"
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
type="info"
showIcon
style={{ marginBottom: '24px', borderRadius: '12px' }}
/>
)}
{/* 表单 */}
<Form
name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'}
size="large"
onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin}
layout="vertical"
requiredMark={false}
>
{registerMode ? (
<>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="username"
label="用户名"
rules={[
{ required: true, message: '请输入用户名' },
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
]}
>
<Input
prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请输入用户名"
style={{ borderRadius: '12px', height: '48px' }}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="realName"
label="真实姓名"
rules={[{ required: true, message: '请输入真实姓名' }]}
>
<Input
prefix={<SafetyCertificateOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请输入真实姓名"
style={{ borderRadius: '12px', height: '48px' }}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="email"
label="邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input
prefix={<MailOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请输入邮箱"
style={{ borderRadius: '12px', height: '48px' }}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="phone" label="手机号">
<Input
prefix={<PhoneOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请输入手机号(可选)"
style={{ borderRadius: '12px', height: '48px' }}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="password"
label="密码"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码长度不能少于6个字符' },
]}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请输入密码"
style={{ borderRadius: '12px', height: '48px' }}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="confirmPassword"
label="确认密码"
dependencies={['password']}
rules={[
{ required: true, message: '请确认密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请再次输入密码"
style={{ borderRadius: '12px', height: '48px' }}
/>
</Form.Item>
</Col>
</Row>
</>
) : (
<>
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请输入用户名"
style={{ borderRadius: '12px', height: '52px' }}
/>
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
style={{ marginBottom: '8px' }}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="请输入密码"
style={{ borderRadius: '12px', height: '52px' }}
/>
</Form.Item>
{!registerMode && !unlockMode && (
<div style={{ textAlign: 'right', marginBottom: '24px' }}>
<Button type="link" style={{ color: '#667eea', padding: 0 }}>
忘记密码
</Button>
</div>
)}
</>
)}
<Form.Item style={{ marginTop: '32px', marginBottom: '16px' }}>
<Button
type="primary"
htmlType="submit"
loading={loading}
block
style={{
height: '52px',
fontSize: '16px',
fontWeight: 600,
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.35)',
}}
>
{registerMode ? '立即注册' : unlockMode ? '立即解锁' : '登 录'}
</Button>
</Form.Item>
</Form>
{/* 底部切换 */}
{!isFirstUser && (
<div style={{ textAlign: 'center', marginTop: '24px' }}>
{!unlockMode && !registerMode && (
<Space split={<Divider type="vertical" />} size="large">
<Button <Button
type="link" type="link"
size="small" onClick={() => setRegisterMode(true)}
style={toggleButtonStyle} style={{ color: '#64748b', fontWeight: 500 }}
onMouseEnter={e => {
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
}}
onMouseLeave={e => {
e.target.style.background = 'transparent';
}}
onClick={() => setUnlockMode(false)}
> >
返回登录 注册新账户
</Button>
</>
) : (
<>
<Button
type="link"
size="small"
style={toggleButtonStyle}
onMouseEnter={e => {
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
}}
onMouseLeave={e => {
e.target.style.background = 'transparent';
}}
onClick={() => setRegisterMode(!registerMode)}
>
{registerMode ? '已有账户?去登录' : '注册新账户'}
</Button> </Button>
<Button <Button
type="link" type="link"
size="small"
style={toggleButtonStyle}
onMouseEnter={e => {
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
}}
onMouseLeave={e => {
e.target.style.background = 'transparent';
}}
onClick={() => setUnlockMode(true)} onClick={() => setUnlockMode(true)}
style={{ color: '#64748b', fontWeight: 500 }}
> >
账户解锁 账户解锁
</Button> </Button>
</> </Space>
)} )}
</Space>
</div> {(registerMode || unlockMode) && (
)} <Text style={{ color: '#64748b' }}>
</Card> 已有账户{' '}
</div> <Button
type="link"
onClick={() => {
setRegisterMode(false);
setUnlockMode(false);
}}
style={{ color: '#667eea', fontWeight: 600, padding: 0 }}
>
立即登录
</Button>
</Text>
)}
</div>
)}
</Card>
{/* 移动端底部版权 */}
<div
style={{
position: 'absolute',
bottom: '24px',
left: 0,
right: 0,
textAlign: 'center',
color: '#94a3b8',
fontSize: '13px',
display: 'none',
}}
className="mobile-footer"
>
© 2024 IDC设备管理系统. All rights reserved.
</div>
</Col>
{/* 响应式样式 */}
<style>{`
@media (max-width: 991px) {
.mobile-bg {
display: block !important;
}
.mobile-footer {
display: block !important;
}
}
@media (max-width: 575px) {
.ant-card-body {
padding: 32px 24px !important;
}
}
`}</style>
</Row>
); );
}; };
+332 -148
View File
@@ -14,6 +14,11 @@ import {
Divider, Divider,
Descriptions, Descriptions,
Alert, Alert,
Row,
Col,
Typography,
Badge,
Tooltip,
} from 'antd'; } from 'antd';
import { import {
SettingOutlined, SettingOutlined,
@@ -22,12 +27,24 @@ import {
InfoCircleOutlined, InfoCircleOutlined,
CheckCircleOutlined, CheckCircleOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
LockOutlined,
DatabaseOutlined,
DesktopOutlined,
ClockCircleOutlined,
MailOutlined,
PhoneOutlined,
EnvironmentOutlined,
FileTextOutlined,
SafetyCertificateOutlined,
ReloadOutlined,
SaveOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import axios from 'axios'; import axios from 'axios';
import { useConfig } from '../context/ConfigContext'; import { useConfig } from '../context/ConfigContext';
const { Option } = Select; const { Option } = Select;
const { TabPane } = Tabs; const { TabPane } = Tabs;
const { Title, Text } = Typography;
const SystemSettings = () => { const SystemSettings = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -362,82 +379,135 @@ const SystemSettings = () => {
return optionsMap[key] || []; return optionsMap[key] || [];
}; };
const renderGeneralSettings = () => { // 设置项分组配置
const generalKeys = [ const settingGroups = {
'site_name', general: [
'site_logo', {
'timezone', title: '站点信息',
'date_format', icon: <GlobalOutlined />,
'idle_timeout', keys: ['site_name', 'site_logo'],
'max_login_attempts', },
'maintenance_mode', {
]; title: '时间设置',
icon: <ClockCircleOutlined />,
keys: ['timezone', 'date_format'],
},
{
title: '安全设置',
icon: <SafetyCertificateOutlined />,
keys: ['idle_timeout', 'max_login_attempts', 'maintenance_mode'],
},
],
appearance: [
{
title: '主题颜色',
icon: <BgColorsOutlined />,
keys: ['primary_color', 'secondary_color'],
},
{
title: '界面布局',
icon: <DesktopOutlined />,
keys: ['compact_mode', 'sidebar_collapsed', 'table_row_height'],
},
{
title: '动画效果',
icon: <CheckCircleOutlined />,
keys: ['animation_enabled'],
},
],
};
const renderSettingGroup = (group) => {
return ( return (
<Card title="全局配置" bordered={false}> <Card
<Form form={form} layout="vertical" onFinish={handleSaveSettings}> key={group.title}
{generalKeys.map(key => { title={
// 确保时区和日期格式使用select类型 <Space>
const settingData = { ...settings[key] }; {group.icon}
if (key === 'timezone' || key === 'date_format') { <span style={{ fontWeight: 600 }}>{group.title}</span>
settingData.type = 'select'; </Space>
} }
return renderFormItem(key, settingData); style={{ marginBottom: 16, borderRadius: 8 }}
})} size="small"
<Form.Item> >
<Space> {group.keys.map(key => {
<Button type="primary" htmlType="submit" loading={saving}> const settingData = { ...settings[key] };
保存设置 // 确保特定字段使用正确的类型
</Button> if (key === 'timezone' || key === 'date_format') {
<Button onClick={() => fetchSettings()}>重置表单</Button> settingData.type = 'select';
</Space> }
</Form.Item> if (key === 'primary_color' || key === 'secondary_color') {
</Form> settingData.type = 'select';
}
return renderFormItem(key, settingData);
})}
</Card> </Card>
); );
}; };
const renderGeneralSettings = () => {
return (
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
{settingGroups.general.map(renderSettingGroup)}
<Card style={{ borderRadius: 8 }} size="small">
<Space>
<Button
type="primary"
htmlType="submit"
loading={saving}
icon={<SaveOutlined />}
size="large"
>
保存设置
</Button>
<Button
onClick={() => fetchSettings()}
icon={<ReloadOutlined />}
size="large"
>
重置表单
</Button>
</Space>
</Card>
</Form>
);
};
const renderAppearanceSettings = () => { const renderAppearanceSettings = () => {
const appearanceKeys = [
'primary_color',
'secondary_color',
'compact_mode',
'sidebar_collapsed',
'table_row_height',
'animation_enabled',
];
return ( return (
<Card title="外观设置" bordered={false}> <Form form={form} layout="vertical" onFinish={handleSaveSettings}>
<Form form={form} layout="vertical" onFinish={handleSaveSettings}> <Alert
<Alert message="主题颜色设置"
message="主题颜色" description="修改主题颜色后需要刷新页面才能生效。建议选择对比度适中的颜色组合。"
description="修改主题颜色后需要刷新页面才能生效。" type="info"
type="info" showIcon
showIcon style={{ marginBottom: 16, borderRadius: 8 }}
style={{ marginBottom: 16 }} />
/> {settingGroups.appearance.map(renderSettingGroup)}
{appearanceKeys.map(key => { <Card style={{ borderRadius: 8 }} size="small">
// 确保主题颜色使用select类型 <Space>
const settingData = { ...settings[key] }; <Button
if (key === 'primary_color' || key === 'secondary_color') { type="primary"
settingData.type = 'select'; htmlType="submit"
} loading={saving}
return renderFormItem(key, settingData); icon={<SaveOutlined />}
})} size="large"
<Form.Item> >
<Space> 保存设置
<Button type="primary" htmlType="submit" loading={saving}> </Button>
保存设置 <Button
</Button> onClick={() => fetchSettings()}
<Button onClick={() => fetchSettings()}>重置表单</Button> icon={<ReloadOutlined />}
</Space> size="large"
</Form.Item> >
</Form> 重置表单
</Card> </Button>
</Space>
</Card>
</Form>
); );
}; };
// 数据备份功能已移除
const renderAboutPage = () => { const renderAboutPage = () => {
const aboutKeys = [ const aboutKeys = [
'app_version', 'app_version',
@@ -452,62 +522,104 @@ const SystemSettings = () => {
return ( return (
<div> <div>
<Card title="关于系统" bordered={false} style={{ marginBottom: 16 }}> {/* 系统概览卡片 */}
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} bordered> <Card
<Descriptions.Item label="系统名称">机柜管理系统</Descriptions.Item> style={{
<Descriptions.Item label="版本号"> marginBottom: 16,
{settings.app_version?.value || '1.0.0'} borderRadius: 8,
</Descriptions.Item> background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
<Descriptions.Item label="系统状态"> color: '#fff',
<Tag color="success">运行正常</Tag> }}
</Descriptions.Item> bodyStyle={{ padding: 24 }}
</Descriptions> >
</Card> <Row gutter={[24, 24]} align="middle">
<Col>
<Card title="公司信息" bordered={false} style={{ marginBottom: 16 }}> <div
<Form layout="vertical"> style={{
{aboutKeys.slice(1).map(key => settings[key] && renderFormItem(key, settings[key]))} width: 80,
<Form.Item> height: 80,
<Space> borderRadius: 16,
<Button backgroundColor: 'rgba(255,255,255,0.2)',
type="primary" display: 'flex',
htmlType="submit" alignItems: 'center',
loading={saving} justifyContent: 'center',
onClick={() => form.submit()} }}
> >
保存信息 <DatabaseOutlined style={{ fontSize: 40, color: '#fff' }} />
</Button> </div>
<Button onClick={() => fetchSettings()}>重置</Button> </Col>
<Col flex="auto">
<Title level={3} style={{ color: '#fff', margin: 0, marginBottom: 8 }}>
机柜管理系统
</Title>
<Space size={16} style={{ color: 'rgba(255,255,255,0.9)' }}>
<span>版本 {settings.app_version?.value || '1.0.0'}</span>
<span>|</span>
<Badge status="success" text="运行正常" style={{ color: '#fff' }} />
</Space> </Space>
</Form.Item> </Col>
</Form> </Row>
</Card> </Card>
{/* 统计信息卡片 */}
{systemInfo && ( {systemInfo && (
<Card title="系统统计信息" bordered={false}> <Card
<Descriptions column={{ xs: 1, sm: 2, md: 4 }} bordered size="small"> title={
<Descriptions.Item label="设备总数"> <Space>
{systemInfo.statistics?.devices || 0} <InfoCircleOutlined />
</Descriptions.Item> <span style={{ fontWeight: 600 }}>系统统计</span>
<Descriptions.Item label="机柜总数"> </Space>
{systemInfo.statistics?.racks || 0} }
</Descriptions.Item> style={{ marginBottom: 16, borderRadius: 8 }}
<Descriptions.Item label="机房总数"> size="small"
{systemInfo.statistics?.rooms || 0} >
</Descriptions.Item> <Row gutter={[16, 16]}>
<Descriptions.Item label="用户总数"> <Col span={6}>
{systemInfo.statistics?.users || 0} <Card size="small" style={{ textAlign: 'center', backgroundColor: '#f6ffed', border: 'none' }}>
</Descriptions.Item> <div style={{ fontSize: 24, fontWeight: 600, color: '#52c41a' }}>
</Descriptions> {systemInfo.statistics?.devices || 0}
<Divider /> </div>
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small"> <div style={{ color: '#666', fontSize: 12 }}>设备总数</div>
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ textAlign: 'center', backgroundColor: '#e6f7ff', border: 'none' }}>
<div style={{ fontSize: 24, fontWeight: 600, color: '#1890ff' }}>
{systemInfo.statistics?.racks || 0}
</div>
<div style={{ color: '#666', fontSize: 12 }}>机柜总数</div>
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ textAlign: 'center', backgroundColor: '#f9f0ff', border: 'none' }}>
<div style={{ fontSize: 24, fontWeight: 600, color: '#722ed1' }}>
{systemInfo.statistics?.rooms || 0}
</div>
<div style={{ color: '#666', fontSize: 12 }}>机房总数</div>
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ textAlign: 'center', backgroundColor: '#fff7e6', border: 'none' }}>
<div style={{ fontSize: 24, fontWeight: 600, color: '#fa8c16' }}>
{systemInfo.statistics?.users || 0}
</div>
<div style={{ color: '#666', fontSize: 12 }}>用户总数</div>
</Card>
</Col>
</Row>
<Divider style={{ margin: '16px 0' }} />
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="Node.js 版本"> <Descriptions.Item label="Node.js 版本">
{systemInfo.system?.nodeVersion} <Tag color="blue">{systemInfo.system?.nodeVersion}</Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="运行平台"> <Descriptions.Item label="运行平台">
{systemInfo.system?.platform} ({systemInfo.system?.arch}) {systemInfo.system?.platform} ({systemInfo.system?.arch})
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="进程 ID">{systemInfo.system?.pid}</Descriptions.Item> <Descriptions.Item label="进程 ID">
<Tag>{systemInfo.system?.pid}</Tag>
</Descriptions.Item>
<Descriptions.Item label="运行时间"> <Descriptions.Item label="运行时间">
{systemInfo.system?.uptime {systemInfo.system?.uptime
? `${Math.floor(systemInfo.system.uptime / 3600)}小时${Math.floor((systemInfo.system.uptime % 3600) / 60)}分钟` ? `${Math.floor(systemInfo.system.uptime / 3600)}小时${Math.floor((systemInfo.system.uptime % 3600) / 60)}分钟`
@@ -526,48 +638,120 @@ const SystemSettings = () => {
</Descriptions> </Descriptions>
</Card> </Card>
)} )}
{/* 公司信息卡片 */}
<Card
title={
<Space>
<FileTextOutlined />
<span style={{ fontWeight: 600 }}>公司信息</span>
</Space>
}
style={{ marginBottom: 16, borderRadius: 8 }}
size="small"
>
<Form layout="vertical">
<Row gutter={[24, 0]}>
<Col span={12}>
<Form.Item label="公司名称" name="company_name">
<Input prefix={<GlobalOutlined />} placeholder="请输入公司名称" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="联系邮箱" name="contact_email">
<Input prefix={<MailOutlined />} placeholder="请输入联系邮箱" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="联系电话" name="contact_phone">
<Input prefix={<PhoneOutlined />} placeholder="请输入联系电话" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="公司地址" name="company_address">
<Input prefix={<EnvironmentOutlined />} placeholder="请输入公司地址" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="系统描述" name="system_description">
<Input.TextArea rows={3} placeholder="请输入系统描述" />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
loading={saving}
onClick={() => form.submit()}
icon={<SaveOutlined />}
>
保存信息
</Button>
<Button onClick={() => fetchSettings()} icon={<ReloadOutlined />}>
重置
</Button>
</Space>
</Form.Item>
</Form>
</Card>
</div> </div>
); );
}; };
const tabItems = [
{
key: 'general',
label: (
<span>
<GlobalOutlined /> 全局配置
</span>
),
children: renderGeneralSettings(),
},
{
key: 'appearance',
label: (
<span>
<BgColorsOutlined /> 外观设置
</span>
),
children: renderAppearanceSettings(),
},
{
key: 'about',
label: (
<span>
<InfoCircleOutlined /> 关于系统
</span>
),
children: renderAboutPage(),
},
];
return ( return (
<div style={{ padding: 24 }}> <div style={{ padding: 24, maxWidth: 1200, margin: '0 auto' }}>
<Tabs activeKey={activeTab} onChange={setActiveTab}> <Card
<TabPane title={
tab={ <Space>
<span> <SettingOutlined style={{ fontSize: 20, color: '#667eea' }} />
<GlobalOutlined /> 全局配置 <Title level={4} style={{ margin: 0 }}>系统设置</Title>
</span> </Space>
} }
key="general" style={{ borderRadius: 12 }}
> bodyStyle={{ padding: 0 }}
{renderGeneralSettings()} >
</TabPane> <Tabs
<TabPane activeKey={activeTab}
tab={ onChange={setActiveTab}
<span> items={tabItems}
<BgColorsOutlined /> 外观设置 style={{ padding: '0 24px 24px' }}
</span> tabBarStyle={{ marginBottom: 24 }}
} />
key="appearance" </Card>
>
{renderAppearanceSettings()}
</TabPane>
<TabPane
tab={
<span>
<InfoCircleOutlined /> 关于
</span>
}
key="about"
>
{renderAboutPage()}
</TabPane>
</Tabs>
</div> </div>
); );
}; };
import { LockOutlined } from '@ant-design/icons';
export default SystemSettings; export default SystemSettings;
+262 -93
View File
@@ -20,6 +20,8 @@ import {
Popover, Popover,
InputNumber, InputNumber,
Switch, Switch,
Row,
Col,
} from 'antd'; } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
@@ -265,6 +267,7 @@ function TicketManagement() {
const [deviceSource, setDeviceSource] = useState('select'); const [deviceSource, setDeviceSource] = useState('select');
const [ticketFields, setTicketFields] = useState(DEFAULT_TICKET_FIELDS); const [ticketFields, setTicketFields] = useState(DEFAULT_TICKET_FIELDS);
const [loadingFields, setLoadingFields] = useState(true); const [loadingFields, setLoadingFields] = useState(true);
const [deviceFields, setDeviceFields] = useState([]);
const fetchTickets = useCallback( const fetchTickets = useCallback(
async (page = 1, pageSize = 10, filters = {}) => { async (page = 1, pageSize = 10, filters = {}) => {
@@ -343,6 +346,17 @@ function TicketManagement() {
} }
}, []); }, []);
// 获取设备字段配置
const fetchDeviceFields = useCallback(async () => {
try {
const response = await axios.get('/api/deviceFields');
setDeviceFields(response.data || []);
} catch (error) {
console.error('获取设备字段配置失败:', error);
setDeviceFields([]);
}
}, []);
useEffect(() => { useEffect(() => {
fetchTickets(); fetchTickets();
fetchDevices(); fetchDevices();
@@ -350,7 +364,8 @@ function TicketManagement() {
// 在 ticketFields 加载完成后再加载分类数据 // 在 ticketFields 加载完成后再加载分类数据
fetchCategories(); fetchCategories();
}); });
}, [fetchTickets, fetchDevices, fetchTicketFields, fetchCategories]); fetchDeviceFields();
}, [fetchTickets, fetchDevices, fetchTicketFields, fetchCategories, fetchDeviceFields]);
// 处理从设备详情页跳转过来创建工单的情况 // 处理从设备详情页跳转过来创建工单的情况
useEffect(() => { useEffect(() => {
@@ -1154,74 +1169,131 @@ function TicketManagement() {
} }
key="device" key="device"
> >
<Descriptions bordered column={2}> <div style={{ padding: '16px 0' }}>
<Descriptions.Item label="设备ID"> {/* 设备基本信息卡片 */}
{selectedTicket.Device.deviceId} <Card title="基本信息" style={{ marginBottom: 16 }} size="small">
</Descriptions.Item> <Row gutter={[24, 16]}>
<Descriptions.Item label="设备名称"> <Col span={8}>
{selectedTicket.Device.name} <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>设备ID</div>
</Descriptions.Item> <div style={{ fontWeight: 500 }}>{selectedTicket.Device.deviceId}</div>
<Descriptions.Item label="设备类型"> </Col>
{selectedTicket.Device.type} <Col span={8}>
</Descriptions.Item> <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>设备名称</div>
<Descriptions.Item label="型号"> <div style={{ fontWeight: 500 }}>{selectedTicket.Device.name}</div>
{selectedTicket.Device.model || '-'} </Col>
</Descriptions.Item> <Col span={8}>
<Descriptions.Item label="序列号"> <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>状态</div>
{selectedTicket.Device.serialNumber || '-'} <Tag
</Descriptions.Item> color={
<Descriptions.Item label="品牌"> selectedTicket.Device.status === 'running'
{selectedTicket.Device.brand || '-'} ? 'green'
</Descriptions.Item> : selectedTicket.Device.status === 'maintenance'
<Descriptions.Item label="所在机房"> ? 'orange'
{selectedTicket.Device.roomName || '-'} : selectedTicket.Device.status === 'fault'
</Descriptions.Item> ? 'red'
<Descriptions.Item label="所在机柜"> : 'default'
{selectedTicket.Device.rackName || '-'} }
</Descriptions.Item> >
<Descriptions.Item label="位置(U)"> {selectedTicket.Device.status === 'running'
{selectedTicket.Device.position || '-'} ? '运行中'
</Descriptions.Item> : selectedTicket.Device.status === 'maintenance'
<Descriptions.Item label="高度(U)"> ? '维护中'
{selectedTicket.Device.height || '-'} : selectedTicket.Device.status === 'fault'
</Descriptions.Item> ? '故障'
<Descriptions.Item label="IP地址"> : selectedTicket.Device.status === 'offline'
{selectedTicket.Device.ipAddress || '-'} ? '离线'
</Descriptions.Item> : selectedTicket.Device.status || '-'}
<Descriptions.Item label="状态"> </Tag>
<Tag </Col>
color={ <Col span={8}>
selectedTicket.Device.status === 'running' <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>设备类型</div>
? 'green' <div>{selectedTicket.Device.type}</div>
: selectedTicket.Device.status === 'maintenance' </Col>
? 'orange' <Col span={8}>
: selectedTicket.Device.status === 'fault' <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>型号</div>
? 'red' <div>{selectedTicket.Device.model || '-'}</div>
: 'default' </Col>
} <Col span={8}>
> <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>序列号</div>
{selectedTicket.Device.status === 'running' <div>{selectedTicket.Device.serialNumber || '-'}</div>
? '运行中' </Col>
: selectedTicket.Device.status === 'maintenance' </Row>
? '维护中' </Card>
: selectedTicket.Device.status === 'fault'
? '故障' {/* 位置信息卡片 */}
: selectedTicket.Device.status === 'offline' <Card title="位置信息" style={{ marginBottom: 16 }} size="small">
? '离线' <Row gutter={[24, 16]}>
: selectedTicket.Device.status || '-'} <Col span={8}>
</Tag> <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>所在机房</div>
</Descriptions.Item> <div>{selectedTicket.Device.roomName || '-'}</div>
<Descriptions.Item label="购买日期"> </Col>
{selectedTicket.Device.purchaseDate <Col span={8}>
? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD') <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>所在机柜</div>
: '-'} <div>{selectedTicket.Device.rackName || '-'}</div>
</Descriptions.Item> </Col>
<Descriptions.Item label="保修到期"> <Col span={8}>
{selectedTicket.Device.warrantyDate <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>IP地址</div>
? dayjs(selectedTicket.Device.warrantyDate).format('YYYY-MM-DD') <div>{selectedTicket.Device.ipAddress || '-'}</div>
: '-'} </Col>
</Descriptions.Item> <Col span={8}>
</Descriptions> <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>位置(U)</div>
<div>{selectedTicket.Device.position || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>高度(U)</div>
<div>{selectedTicket.Device.height || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>功耗(W)</div>
<div>{selectedTicket.Device.powerConsumption || '-'}</div>
</Col>
</Row>
</Card>
{/* 维保信息卡片 */}
<Card title="维保信息" style={{ marginBottom: 16 }} size="small">
<Row gutter={[24, 16]}>
<Col span={12}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>购买日期</div>
<div>{selectedTicket.Device.purchaseDate
? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD')
: '-'}</div>
</Col>
<Col span={12}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>保修到期</div>
<div>{selectedTicket.Device.warrantyExpiry
? dayjs(selectedTicket.Device.warrantyExpiry).format('YYYY-MM-DD')
: '-'}</div>
</Col>
</Row>
</Card>
{/* 描述信息 */}
{selectedTicket.Device.description && (
<Card title="描述" style={{ marginBottom: 16 }} size="small">
<div style={{ whiteSpace: 'pre-wrap' }}>{selectedTicket.Device.description}</div>
</Card>
)}
{/* 自定义字段卡片 */}
{selectedTicket.Device.customFields && Object.keys(selectedTicket.Device.customFields).length > 0 && (
<Card title="自定义字段" size="small">
<Row gutter={[24, 16]}>
{Object.entries(selectedTicket.Device.customFields).map(([key, value]) => {
// 从 deviceFields 中查找对应的中文显示名称
const fieldConfig = deviceFields.find(f => f.fieldName === key);
const displayName = fieldConfig?.displayName || key;
return (
<Col span={8} key={key}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>{displayName}</div>
<div style={{ fontWeight: 500 }}>{String(value)}</div>
</Col>
);
})}
</Row>
</Card>
)}
</div>
</TabPane> </TabPane>
)} )}
@@ -1233,30 +1305,127 @@ function TicketManagement() {
} }
key="operations" key="operations"
> >
<Timeline mode="left"> <div style={{ padding: '16px 0' }}>
{operationRecords.map((record, index) => ( {operationRecords.length === 0 ? (
<Timeline.Item <div style={{ textAlign: 'center', padding: '40px 0', color: '#888' }}>
key={index} <ClockCircleOutlined style={{ fontSize: 48, marginBottom: 16 }} />
label={dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')} <p>暂无操作记录</p>
color={ </div>
record.operationType === 'create' ) : (
? 'green' <div style={{ position: 'relative' }}>
: record.operationType === 'complete' {/* 时间线轴线 */}
? 'blue' <div
: record.operationType === 'close' style={{
? 'gray' position: 'absolute',
: 'orange' left: '20px',
} top: '0',
> bottom: '0',
<div> width: '2px',
<strong>{record.operationType}</strong> backgroundColor: '#e8e8e8',
</div> }}
<div>操作人: {record.operatorName || '-'}</div> />
<div>内容: {record.operationDescription || '-'}</div> {operationRecords.map((record, index) => {
</Timeline.Item> const getOperationInfo = type => {
))} switch (type) {
{operationRecords.length === 0 && <p style={{ color: '#888' }}>暂无操作记录</p>} case 'create':
</Timeline> return { color: '#52c41a', bgColor: '#f6ffed', label: '创建工单' };
case 'complete':
return { color: '#1890ff', bgColor: '#e6f7ff', label: '完成工单' };
case 'close':
return { color: '#8c8c8c', bgColor: '#f5f5f5', label: '关闭工单' };
case 'assign':
return { color: '#722ed1', bgColor: '#f9f0ff', label: '分配工单' };
case 'update':
return { color: '#fa8c16', bgColor: '#fff7e6', label: '更新工单' };
default:
return { color: '#fa8c16', bgColor: '#fff7e6', label: type };
}
};
const info = getOperationInfo(record.operationType);
return (
<div
key={index}
style={{
position: 'relative',
paddingLeft: '48px',
marginBottom: '24px',
}}
>
{/* 时间点圆点 */}
<div
style={{
position: 'absolute',
left: '12px',
top: '4px',
width: '16px',
height: '16px',
borderRadius: '50%',
backgroundColor: info.color,
border: '3px solid #fff',
boxShadow: '0 0 0 2px ' + info.color + '40',
}}
/>
{/* 操作卡片 */}
<Card
size="small"
style={{
backgroundColor: info.bgColor,
border: 'none',
borderRadius: '8px',
}}
bodyStyle={{ padding: '12px 16px' }}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '8px',
}}
>
<Tag
color={info.color}
style={{
fontSize: '12px',
fontWeight: 500,
border: 'none',
margin: 0,
}}
>
{info.label}
</Tag>
<span style={{ color: '#666', fontSize: '12px' }}>
{dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</span>
</div>
<div style={{ marginBottom: '4px' }}>
<span style={{ color: '#666' }}>操作人</span>
<span style={{ fontWeight: 500 }}>
{record.operatorName || '-'}
</span>
</div>
{record.operationDescription && (
<div
style={{
marginTop: '8px',
padding: '8px 12px',
backgroundColor: '#fff',
borderRadius: '4px',
fontSize: '13px',
color: '#333',
lineHeight: '1.5',
}}
>
{record.operationDescription}
</div>
)}
</Card>
</div>
);
})}
</div>
)}
</div>
</TabPane> </TabPane>
</Tabs> </Tabs>
)} )}