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 Device = require('../models/Device');
const User = require('../models/User');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const { dbDialect } = require('../db');
// 获取工单统计 (必须定义在 /:ticketId 之前)
@@ -238,7 +240,21 @@ router.get('/:ticketId', async (req, res) => {
const ticket = await Ticket.findByPk(req.params.ticketId, {
include: [
{ 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']] }
]
});
@@ -247,7 +263,14 @@ router.get('/:ticketId', async (req, res) => {
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) {
res.status(500).json({ error: error.message });
}
+180 -158
View File
@@ -12,6 +12,9 @@ import {
Popconfirm,
Table,
Badge,
Row,
Col,
Divider,
} from 'antd';
import {
ApiOutlined,
@@ -23,22 +26,29 @@ import {
FileTextOutlined,
ToolOutlined,
EyeOutlined,
DesktopOutlined,
FieldTimeOutlined,
InfoCircleOutlined,
LinkOutlined,
} from '@ant-design/icons';
import NetworkCardPanel from './NetworkCardPanel';
import { deviceAPI } from '../api';
import dayjs from 'dayjs';
const { Text, Title } = Typography;
const designTokens = {
colors: {
primary: '#667eea',
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
},
spacing: {
sm: 8,
md: 16,
primary: '#1890ff',
success: '#52c41a',
error: '#f5222d',
warning: '#faad14',
info: '#13c2c2',
gray: '#8c8c8c',
bgLight: '#f6ffed',
bgBlue: '#e6f7ff',
bgGray: '#f5f5f5',
bgOrange: '#fff7e6',
},
};
@@ -150,8 +160,7 @@ function DeviceDetailDrawer({
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')}`;
return dayjs(date).format('YYYY-MM-DD HH:mm');
},
},
{
@@ -205,49 +214,12 @@ function DeviceDetailDrawer({
return typeMap[type?.toLowerCase()] || type || '未知设备';
}, []);
const renderFieldValue = useCallback(
(field, device) => {
const fieldKey = field.field;
if (fieldKey === 'status') return getStatusTag(device.status);
if (!device) return null;
// 优先从device对象获取值,如果没有则从customFields中获取
let value = device[fieldKey];
if (
(value === undefined || value === null) &&
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 customFields = device.customFields || {};
const standardFields = ['deviceId', 'name', 'type', 'model', 'serialNumber', 'status', 'ipAddress', 'position', 'height', 'powerConsumption', 'purchaseDate', 'warrantyExpiry', 'description'];
const customFieldEntries = Object.entries(customFields).filter(([key]) => !standardFields.includes(key));
const tabItems = [
{
@@ -271,7 +243,7 @@ function DeviceDetailDrawer({
key: 'cables',
label: (
<span>
<EnvironmentOutlined />
<LinkOutlined />
接线 ({deviceCables.length})
</span>
),
@@ -280,7 +252,7 @@ function DeviceDetailDrawer({
{deviceCables.length === 0 ? (
<Empty description="该设备暂无接线" />
) : (
<Space direction="vertical" size={designTokens.spacing.md} style={{ width: '100%' }}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
{deviceCables.map(cable => (
<Card
key={cable.cableId}
@@ -297,70 +269,60 @@ function DeviceDetailDrawer({
</Popconfirm>
}
>
<div style={{ marginBottom: designTokens.spacing.sm }}>
<Space direction="vertical" size={4}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>
源设备
</Text>
<div style={{ fontWeight: 500 }}>
{cable.sourceDevice?.name || '-'}
<Tag color="blue" style={{ marginLeft: '8px' }}>
{cable.sourcePort}
</Tag>
</div>
<Row gutter={[16, 8]}>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>源设备</div>
<div style={{ fontWeight: 500 }}>
{cable.sourceDevice?.name || '-'}
<Tag color="blue" style={{ marginLeft: '8px' }}>
{cable.sourcePort}
</Tag>
</div>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>
目标设备
</Text>
<div style={{ fontWeight: 500 }}>
{cable.targetDevice?.name || '-'}
<Tag color="green" style={{ marginLeft: '8px' }}>
{cable.targetPort}
</Tag>
</div>
</Col>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: 4 }}>目标设备</div>
<div style={{ fontWeight: 500 }}>
{cable.targetDevice?.name || '-'}
<Tag color="green" style={{ marginLeft: '8px' }}>
{cable.targetPort}
</Tag>
</div>
</Space>
</div>
<Space wrap>
<Tag
color={
cable.status === 'normal'
? 'success'
: cable.status === 'fault'
? 'error'
: 'default'
}
>
{cable.status === 'normal'
? '正常'
: cable.status === 'fault'
? '故障'
: '未连接'}
</Tag>
<Tag color="purple">
{cable.cableType === 'ethernet'
? '网线'
: cable.cableType === 'fiber'
? '光纤'
: '铜缆'}
</Tag>
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
</Space>
{cable.description && (
<div
style={{
marginTop: designTokens.spacing.sm,
fontSize: '12px',
color: '#666',
}}
>
{cable.description}
</div>
)}
</Col>
<Col span={24}>
<Space wrap style={{ marginTop: 8 }}>
<Tag
color={
cable.status === 'normal'
? 'success'
: cable.status === 'fault'
? 'error'
: 'default'
}
>
{cable.status === 'normal'
? '正常'
: cable.status === 'fault'
? '故障'
: '未连接'}
</Tag>
<Tag color="purple">
{cable.cableType === 'ethernet'
? '网线'
: cable.cableType === 'fiber'
? '光纤'
: '铜缆'}
</Tag>
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
</Space>
</Col>
{cable.description && (
<Col span={24}>
<div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
{cable.description}
</div>
</Col>
)}
</Row>
</Card>
))}
</Space>
@@ -378,7 +340,7 @@ function DeviceDetailDrawer({
),
children: (
<div className="tickets-panel">
<div style={{ marginBottom: designTokens.spacing.md }}>
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
icon={<ToolOutlined />}
@@ -403,32 +365,24 @@ function DeviceDetailDrawer({
},
];
if (!device) return null;
return (
<Drawer
title={
<Space style={{ maxWidth: '280px', overflow: 'hidden' }}>
<CloudServerOutlined style={{ color: designTokens.colors.primary, flexShrink: 0 }} />
<span
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
设备详情 - {device.name}
</span>
<Space>
<CloudServerOutlined style={{ color: designTokens.colors.primary, fontSize: 20 }} />
<span style={{ fontSize: 18, fontWeight: 600 }}>设备详情</span>
</Space>
}
placement="right"
width={600}
width={700}
open={visible}
onClose={onClose}
extra={
<Space>
<Tooltip title="编辑设备信息">
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)} />
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)}>
编辑
</Button>
</Tooltip>
<Tooltip title="添加网卡">
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>
@@ -447,35 +401,103 @@ function DeviceDetailDrawer({
</Tooltip>
</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' }}>
基本信息
</Title>
<div
className="info-grid"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '12px',
background: '#f8fafc',
padding: '16px',
borderRadius: '10px',
}}
>
{displayFields.map(field => (
<div className="info-item" key={field.field}>
<Text type="secondary" style={{ fontSize: '12px', display: 'block' }}>
{field.label}
</Text>
{renderFieldValue(field, device)}
</div>
))}
</div>
{/* 设备头部信息 */}
<div style={{ padding: '20px 24px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: '#fff' }}>
<Row gutter={[16, 16]} align="middle">
<Col>
<DesktopOutlined style={{ fontSize: 48, opacity: 0.9 }} />
</Col>
<Col flex="auto">
<div style={{ fontSize: 24, fontWeight: 600, marginBottom: 8 }}>{device.name}</div>
<Space size={16}>
<span>{getDeviceTypeName(device.type)}</span>
<span>|</span>
<span>{device.deviceId}</span>
<span>|</span>
{getStatusTag(device.status)}
</Space>
</Col>
</Row>
</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>
);
}
+155 -145
View File
@@ -18,6 +18,9 @@ import {
Spin,
Dropdown,
Tooltip,
Tag,
Row,
Col,
} from 'antd';
import {
PlusOutlined,
@@ -2154,7 +2157,7 @@ function DeviceManagement() {
<Modal
title={
<div style={modalHeaderStyle}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: 600 }}>
<AppstoreOutlined style={{ color: '#667eea' }} />
设备详情
</div>
@@ -2216,161 +2219,168 @@ function DeviceManagement() {
编辑
</Button>,
]}
width={800}
width={700}
destroyOnHidden
styles={{
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
body: { padding: '24px' },
body: { padding: '0', overflow: 'auto' },
}}
>
{selectedDevice && (
<div>
<Card
size="small"
title={
<span style={{ fontWeight: '600' }}>
<CloudServerOutlined style={{ marginRight: '8px', color: '#667eea' }} />
基本信息
</span>
}
style={{ borderRadius: '12px', border: '1px solid #f0f0f0' }}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px' }}>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>设备ID</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.deviceId || '-'}
</span>
{/* 头部信息区域 */}
<div style={{
padding: '24px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#fff'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{
width: '64px',
height: '64px',
borderRadius: '12px',
backgroundColor: 'rgba(255,255,255,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
{getDeviceTypeIcon(selectedDevice.type)}
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>设备名称</label>
<span style={{ marginLeft: 8, color: '#333' }}>{selectedDevice.name || '-'}</span>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>设备类型</label>
<span style={{ marginLeft: 8 }}>
{selectedDevice.type ? (
<Space>
{getDeviceTypeIcon(selectedDevice.type)}
<span>{getTypeLabel(selectedDevice.type)}</span>
</Space>
) : (
'-'
)}
</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 style={{ flex: 1 }}>
<div style={{ fontSize: '24px', fontWeight: 600, marginBottom: '8px' }}>
{selectedDevice.name}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', opacity: 0.9 }}>
<span>{getTypeLabel(selectedDevice.type)}</span>
<span>|</span>
<span>{selectedDevice.deviceId}</span>
<span>|</span>
<Tag color={selectedDevice.status ? getStatusConfig(selectedDevice.status).badgeColor : 'default'} style={{ margin: 0 }}>
{selectedDevice.status ? getStatusConfig(selectedDevice.status).text : '-'}
</Tag>
</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>
)}
</Modal>
+505 -324
View File
@@ -1,18 +1,31 @@
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 {
UserOutlined,
LockOutlined,
MailOutlined,
PhoneOutlined,
SafetyCertificateOutlined,
RobotOutlined,
CloudServerOutlined,
ArrowLeftOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { authAPI } from '../api';
const { Title, Text } = Typography;
const { Title, Text, Paragraph } = Typography;
const Login = () => {
const [loading, setLoading] = useState(false);
@@ -115,359 +128,527 @@ const Login = () => {
}
};
const containerStyle = {
minHeight: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)',
padding: '24px',
position: 'relative',
overflow: 'hidden',
};
const backgroundDecorationStyle = {
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}>
// 左侧宣传区域组件
const LeftPanel = () => (
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '60px',
color: '#fff',
position: 'relative',
overflow: 'hidden',
}}
>
{/* 背景装饰 */}
<div
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',
height: '400px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
top: '-100px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.08)',
bottom: '-100px',
right: '-100px',
}}
/>
<div
style={{
...backgroundDecorationStyle,
width: '300px',
height: '300px',
background: 'linear-gradient(135deg, #764ba2 0%, #6B8DD6 100%)',
bottom: '-50px',
left: '-50px',
filter: 'blur(40px)',
}}
/>
<Card style={cardStyle}>
<div style={headerStyle}>
<div style={iconContainerStyle}>
<RobotOutlined style={{ fontSize: '40px', color: '#fff' }} />
</div>
<Title level={2} style={titleStyle}>
{isFirstUser ? '创建管理员账户' : unlockMode ? '账户解锁' : 'IDC设备管理系统'}
</Title>
<Text style={subtitleStyle}>
{isFirstUser
? '首次使用,请创建系统管理员账户'
: unlockMode
? '输入账户信息以解锁账户'
: '安全登录您的账户'}
</Text>
<div style={{ position: 'relative', zIndex: 1 }}>
<div
style={{
width: '80px',
height: '80px',
borderRadius: '20px',
background: 'rgba(255,255,255,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '40px',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.3)',
}}
>
<CloudServerOutlined style={{ fontSize: '40px', color: '#fff' }} />
</div>
{isFirstUser && (
<Alert
message="欢迎使用IDC设备管理系统"
description="您是第一个用户,系统将自动为您分配管理员权限。"
type="success"
showIcon
style={{ marginBottom: '24px', borderRadius: '8px' }}
/>
)}
<Form
name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'}
size="large"
onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin}
style={formStyle}
<Title
level={1}
style={{
color: '#fff',
fontSize: '48px',
fontWeight: 700,
marginBottom: '24px',
lineHeight: 1.2,
}}
>
{registerMode ? (
<>
<Form.Item
name="username"
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>
IDC设备
<br />
管理系统
</Title>
<Form.Item name="realName" rules={[{ required: true, message: '请输入真实姓名' }]}>
<Input
prefix={<SafetyCertificateOutlined style={inputPrefixStyle} />}
placeholder="真实姓名"
style={inputStyle}
/>
</Form.Item>
<Paragraph
style={{
color: 'rgba(255,255,255,0.85)',
fontSize: '18px',
lineHeight: 1.8,
maxWidth: '480px',
marginBottom: '48px',
}}
>
专业的数据中心设备管理平台提供机房机柜设备的全生命周期管理
助力企业实现高效的IT资产管理
</Paragraph>
<Form.Item
name="email"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input
prefix={<MailOutlined style={inputPrefixStyle} />}
placeholder="邮箱"
style={inputStyle}
/>
</Form.Item>
<Row gutter={[24, 24]}>
<Col xs={8} sm={8} md={8}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>99.9%</div>
<div style={{ fontSize: '13px', opacity: 0.8 }}>系统稳定性</div>
</div>
</Col>
<Col xs={8} sm={8} md={8}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>24/7</div>
<div style={{ fontSize: '13px', opacity: 0.8 }}>全天候监控</div>
</div>
</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
prefix={<PhoneOutlined style={inputPrefixStyle} />}
placeholder="手机号(可选)"
style={inputStyle}
/>
</Form.Item>
// 获取标题和副标题
const getHeaderContent = () => {
if (isFirstUser) {
return {
title: '创建管理员账户',
subtitle: '首次使用,请创建系统管理员账户',
};
}
if (unlockMode) {
return {
title: '账户解锁',
subtitle: '输入账户信息以解锁账户',
};
}
if (registerMode) {
return {
title: '注册新账户',
subtitle: '填写信息完成账户注册',
};
}
return {
title: '欢迎回来',
subtitle: '请登录您的账户以继续',
};
};
<Form.Item
name="password"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码长度不能少于6个字符' },
]}
>
<Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />}
placeholder="密码"
style={inputStyle}
/>
</Form.Item>
const headerContent = getHeaderContent();
<Form.Item
name="confirmPassword"
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={inputPrefixStyle} />}
placeholder="确认密码"
style={inputStyle}
/>
</Form.Item>
</>
) : unlockMode ? (
<>
<Alert
message="账户解锁说明"
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
type="info"
showIcon
style={{ marginBottom: '24px', borderRadius: '8px' }}
/>
return (
<Row style={{ minHeight: '100vh', overflow: 'hidden' }}>
{/* 左侧区域 - 桌面端显示 */}
<Col
xs={0}
sm={0}
md={0}
lg={12}
xl={14}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)',
}}
>
<LeftPanel />
</Col>
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input
prefix={<UserOutlined style={inputPrefixStyle} />}
placeholder="用户名"
style={inputStyle}
/>
</Form.Item>
{/* 右侧登录区域 */}
<Col
xs={24}
sm={24}
md={24}
lg={12}
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: '请输入密码' }]}>
<Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />}
placeholder="密码"
style={inputStyle}
/>
</Form.Item>
</>
) : (
<>
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input
prefix={<UserOutlined style={inputPrefixStyle} />}
placeholder="用户名"
style={inputStyle}
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />}
placeholder="密码"
style={inputStyle}
/>
</Form.Item>
</>
<Card
style={{
width: '100%',
maxWidth: registerMode ? 520 : 440,
borderRadius: '24px',
boxShadow: '0 25px 80px rgba(0,0,0,0.15), 0 10px 30px rgba(0,0,0,0.1)',
background: '#fff',
border: 'none',
position: 'relative',
zIndex: 1,
}}
bodyStyle={{ padding: '48px' }}
>
{/* 返回按钮 */}
{(registerMode || unlockMode) && !isFirstUser && (
<Button
type="link"
icon={<ArrowLeftOutlined />}
onClick={() => {
setRegisterMode(false);
setUnlockMode(false);
}}
style={{
position: 'absolute',
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}>
{registerMode ? '立即注册' : unlockMode ? '解 锁' : '登 录'}
</Button>
</Form.Item>
</Form>
{/* 头部 */}
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div
style={{
width: '64px',
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}>
<Divider plain>
<Text style={{ color: '#8c8c8c', fontSize: '12px' }}>其他方式</Text>
</Divider>
<Space split={<Divider type="vertical" />}>
{unlockMode ? (
<>
{/* 首次使用提示 */}
{isFirstUser && (
<Alert
message="欢迎使用IDC设备管理系统"
description="您是第一个用户,系统将自动为您分配管理员权限。"
type="success"
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
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(false)}
onClick={() => setRegisterMode(true)}
style={{ color: '#64748b', fontWeight: 500 }}
>
返回登录
</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
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)}
style={{ color: '#64748b', fontWeight: 500 }}
>
账户解锁
</Button>
</>
</Space>
)}
</Space>
</div>
)}
</Card>
</div>
{(registerMode || unlockMode) && (
<Text style={{ color: '#64748b' }}>
已有账户{' '}
<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,
Descriptions,
Alert,
Row,
Col,
Typography,
Badge,
Tooltip,
} from 'antd';
import {
SettingOutlined,
@@ -22,12 +27,24 @@ import {
InfoCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
LockOutlined,
DatabaseOutlined,
DesktopOutlined,
ClockCircleOutlined,
MailOutlined,
PhoneOutlined,
EnvironmentOutlined,
FileTextOutlined,
SafetyCertificateOutlined,
ReloadOutlined,
SaveOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import { useConfig } from '../context/ConfigContext';
const { Option } = Select;
const { TabPane } = Tabs;
const { Title, Text } = Typography;
const SystemSettings = () => {
const [loading, setLoading] = useState(false);
@@ -362,82 +379,135 @@ const SystemSettings = () => {
return optionsMap[key] || [];
};
const renderGeneralSettings = () => {
const generalKeys = [
'site_name',
'site_logo',
'timezone',
'date_format',
'idle_timeout',
'max_login_attempts',
'maintenance_mode',
];
// 设置项分组配置
const settingGroups = {
general: [
{
title: '站点信息',
icon: <GlobalOutlined />,
keys: ['site_name', 'site_logo'],
},
{
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 (
<Card title="全局配置" bordered={false}>
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
{generalKeys.map(key => {
// 确保时区和日期格式使用select类型
const settingData = { ...settings[key] };
if (key === 'timezone' || key === 'date_format') {
settingData.type = 'select';
}
return renderFormItem(key, settingData);
})}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving}>
保存设置
</Button>
<Button onClick={() => fetchSettings()}>重置表单</Button>
</Space>
</Form.Item>
</Form>
<Card
key={group.title}
title={
<Space>
{group.icon}
<span style={{ fontWeight: 600 }}>{group.title}</span>
</Space>
}
style={{ marginBottom: 16, borderRadius: 8 }}
size="small"
>
{group.keys.map(key => {
const settingData = { ...settings[key] };
// 确保特定字段使用正确的类型
if (key === 'timezone' || key === 'date_format') {
settingData.type = 'select';
}
if (key === 'primary_color' || key === 'secondary_color') {
settingData.type = 'select';
}
return renderFormItem(key, settingData);
})}
</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 appearanceKeys = [
'primary_color',
'secondary_color',
'compact_mode',
'sidebar_collapsed',
'table_row_height',
'animation_enabled',
];
return (
<Card title="外观设置" bordered={false}>
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
<Alert
message="主题颜色"
description="修改主题颜色后需要刷新页面才能生效。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
{appearanceKeys.map(key => {
// 确保主题颜色使用select类型
const settingData = { ...settings[key] };
if (key === 'primary_color' || key === 'secondary_color') {
settingData.type = 'select';
}
return renderFormItem(key, settingData);
})}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving}>
保存设置
</Button>
<Button onClick={() => fetchSettings()}>重置表单</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
<Alert
message="主题颜色设置"
description="修改主题颜色后需要刷新页面才能生效。建议选择对比度适中的颜色组合。"
type="info"
showIcon
style={{ marginBottom: 16, borderRadius: 8 }}
/>
{settingGroups.appearance.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 renderAboutPage = () => {
const aboutKeys = [
'app_version',
@@ -452,62 +522,104 @@ const SystemSettings = () => {
return (
<div>
<Card title="关于系统" bordered={false} style={{ marginBottom: 16 }}>
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} bordered>
<Descriptions.Item label="系统名称">机柜管理系统</Descriptions.Item>
<Descriptions.Item label="版本号">
{settings.app_version?.value || '1.0.0'}
</Descriptions.Item>
<Descriptions.Item label="系统状态">
<Tag color="success">运行正常</Tag>
</Descriptions.Item>
</Descriptions>
</Card>
<Card title="公司信息" bordered={false} style={{ marginBottom: 16 }}>
<Form layout="vertical">
{aboutKeys.slice(1).map(key => settings[key] && renderFormItem(key, settings[key]))}
<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
loading={saving}
onClick={() => form.submit()}
>
保存信息
</Button>
<Button onClick={() => fetchSettings()}>重置</Button>
{/* 系统概览卡片 */}
<Card
style={{
marginBottom: 16,
borderRadius: 8,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#fff',
}}
bodyStyle={{ padding: 24 }}
>
<Row gutter={[24, 24]} align="middle">
<Col>
<div
style={{
width: 80,
height: 80,
borderRadius: 16,
backgroundColor: 'rgba(255,255,255,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<DatabaseOutlined style={{ fontSize: 40, color: '#fff' }} />
</div>
</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>
</Form.Item>
</Form>
</Col>
</Row>
</Card>
{/* 统计信息卡片 */}
{systemInfo && (
<Card title="系统统计信息" bordered={false}>
<Descriptions column={{ xs: 1, sm: 2, md: 4 }} bordered size="small">
<Descriptions.Item label="设备总数">
{systemInfo.statistics?.devices || 0}
</Descriptions.Item>
<Descriptions.Item label="机柜总数">
{systemInfo.statistics?.racks || 0}
</Descriptions.Item>
<Descriptions.Item label="机房总数">
{systemInfo.statistics?.rooms || 0}
</Descriptions.Item>
<Descriptions.Item label="用户总数">
{systemInfo.statistics?.users || 0}
</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small">
<Card
title={
<Space>
<InfoCircleOutlined />
<span style={{ fontWeight: 600 }}>系统统计</span>
</Space>
}
style={{ marginBottom: 16, borderRadius: 8 }}
size="small"
>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ textAlign: 'center', backgroundColor: '#f6ffed', border: 'none' }}>
<div style={{ fontSize: 24, fontWeight: 600, color: '#52c41a' }}>
{systemInfo.statistics?.devices || 0}
</div>
<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 版本">
{systemInfo.system?.nodeVersion}
<Tag color="blue">{systemInfo.system?.nodeVersion}</Tag>
</Descriptions.Item>
<Descriptions.Item label="运行平台">
{systemInfo.system?.platform} ({systemInfo.system?.arch})
</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="运行时间">
{systemInfo.system?.uptime
? `${Math.floor(systemInfo.system.uptime / 3600)}小时${Math.floor((systemInfo.system.uptime % 3600) / 60)}分钟`
@@ -526,48 +638,120 @@ const SystemSettings = () => {
</Descriptions>
</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>
);
};
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 (
<div style={{ padding: 24 }}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane
tab={
<span>
<GlobalOutlined /> 全局配置
</span>
}
key="general"
>
{renderGeneralSettings()}
</TabPane>
<TabPane
tab={
<span>
<BgColorsOutlined /> 外观设置
</span>
}
key="appearance"
>
{renderAppearanceSettings()}
</TabPane>
<TabPane
tab={
<span>
<InfoCircleOutlined /> 关于
</span>
}
key="about"
>
{renderAboutPage()}
</TabPane>
</Tabs>
<div style={{ padding: 24, maxWidth: 1200, margin: '0 auto' }}>
<Card
title={
<Space>
<SettingOutlined style={{ fontSize: 20, color: '#667eea' }} />
<Title level={4} style={{ margin: 0 }}>系统设置</Title>
</Space>
}
style={{ borderRadius: 12 }}
bodyStyle={{ padding: 0 }}
>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
style={{ padding: '0 24px 24px' }}
tabBarStyle={{ marginBottom: 24 }}
/>
</Card>
</div>
);
};
import { LockOutlined } from '@ant-design/icons';
export default SystemSettings;
+262 -93
View File
@@ -20,6 +20,8 @@ import {
Popover,
InputNumber,
Switch,
Row,
Col,
} from 'antd';
import {
PlusOutlined,
@@ -265,6 +267,7 @@ function TicketManagement() {
const [deviceSource, setDeviceSource] = useState('select');
const [ticketFields, setTicketFields] = useState(DEFAULT_TICKET_FIELDS);
const [loadingFields, setLoadingFields] = useState(true);
const [deviceFields, setDeviceFields] = useState([]);
const fetchTickets = useCallback(
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(() => {
fetchTickets();
fetchDevices();
@@ -350,7 +364,8 @@ function TicketManagement() {
// 在 ticketFields 加载完成后再加载分类数据
fetchCategories();
});
}, [fetchTickets, fetchDevices, fetchTicketFields, fetchCategories]);
fetchDeviceFields();
}, [fetchTickets, fetchDevices, fetchTicketFields, fetchCategories, fetchDeviceFields]);
// 处理从设备详情页跳转过来创建工单的情况
useEffect(() => {
@@ -1154,74 +1169,131 @@ function TicketManagement() {
}
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>
<div style={{ padding: '16px 0' }}>
{/* 设备基本信息卡片 */}
<Card title="基本信息" style={{ marginBottom: 16 }} size="small">
<Row gutter={[24, 16]}>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>设备ID</div>
<div style={{ fontWeight: 500 }}>{selectedTicket.Device.deviceId}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>设备名称</div>
<div style={{ fontWeight: 500 }}>{selectedTicket.Device.name}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>状态</div>
<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>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>设备类型</div>
<div>{selectedTicket.Device.type}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>型号</div>
<div>{selectedTicket.Device.model || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>序列号</div>
<div>{selectedTicket.Device.serialNumber || '-'}</div>
</Col>
</Row>
</Card>
{/* 位置信息卡片 */}
<Card title="位置信息" style={{ marginBottom: 16 }} size="small">
<Row gutter={[24, 16]}>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>所在机房</div>
<div>{selectedTicket.Device.roomName || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>所在机柜</div>
<div>{selectedTicket.Device.rackName || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>IP地址</div>
<div>{selectedTicket.Device.ipAddress || '-'}</div>
</Col>
<Col span={8}>
<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>
)}
@@ -1233,30 +1305,127 @@ function TicketManagement() {
}
key="operations"
>
<Timeline mode="left">
{operationRecords.map((record, index) => (
<Timeline.Item
key={index}
label={dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')}
color={
record.operationType === 'create'
? 'green'
: record.operationType === 'complete'
? 'blue'
: record.operationType === 'close'
? 'gray'
: 'orange'
}
>
<div>
<strong>{record.operationType}</strong>
</div>
<div>操作人: {record.operatorName || '-'}</div>
<div>内容: {record.operationDescription || '-'}</div>
</Timeline.Item>
))}
{operationRecords.length === 0 && <p style={{ color: '#888' }}>暂无操作记录</p>}
</Timeline>
<div style={{ padding: '16px 0' }}>
{operationRecords.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px 0', color: '#888' }}>
<ClockCircleOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<p>暂无操作记录</p>
</div>
) : (
<div style={{ position: 'relative' }}>
{/* 时间线轴线 */}
<div
style={{
position: 'absolute',
left: '20px',
top: '0',
bottom: '0',
width: '2px',
backgroundColor: '#e8e8e8',
}}
/>
{operationRecords.map((record, index) => {
const getOperationInfo = type => {
switch (type) {
case 'create':
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>
</Tabs>
)}