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 });
} }
+141 -119
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,34 +269,27 @@ 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' }}>
源设备
</Text>
<div style={{ fontWeight: 500 }}> <div style={{ fontWeight: 500 }}>
{cable.sourceDevice?.name || '-'} {cable.sourceDevice?.name || '-'}
<Tag color="blue" style={{ marginLeft: '8px' }}> <Tag color="blue" style={{ marginLeft: '8px' }}>
{cable.sourcePort} {cable.sourcePort}
</Tag> </Tag>
</div> </div>
</div> </Col>
<div> <Col span={12}>
<Text type="secondary" style={{ fontSize: '12px' }}> <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> </Col>
</Space> <Col span={24}>
</div> <Space wrap style={{ marginTop: 8 }}>
<Space wrap>
<Tag <Tag
color={ color={
cable.status === 'normal' cable.status === 'normal'
@@ -349,18 +314,15 @@ function DeviceDetailDrawer({
</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 && (
<div <Col span={24}>
style={{ <div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
marginTop: designTokens.spacing.sm,
fontSize: '12px',
color: '#666',
}}
>
{cable.description} {cable.description}
</div> </div>
</Col>
)} )}
</Row>
</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>
{/* 设备信息内容 */}
<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} /> <Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
</div>
</Drawer> </Drawer>
); );
} }
+141 -131
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>
{/* 头部信息区域 */}
<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 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 <Card
size="small" size="small"
title={ title={<span style={{ fontWeight: 600 }}>基本信息</span>}
<span style={{ fontWeight: '600' }}> style={{ marginBottom: '16px', borderRadius: '8px' }}
<CloudServerOutlined style={{ marginRight: '8px', color: '#667eea' }} />
基本信息
</span>
}
style={{ borderRadius: '12px', border: '1px solid #f0f0f0' }}
> >
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px' }}> <Row gutter={[24, 16]}>
<div> <Col span={8}>
<label style={{ fontWeight: '500', color: '#666' }}>设备ID</label> <div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>设备型号</div>
<span style={{ marginLeft: 8, color: '#333' }}> <div style={{ fontWeight: 500 }}>{selectedDevice.model || '-'}</div>
{selectedDevice.deviceId || '-'} </Col>
</span> <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> </div>
<div> </Col>
<label style={{ fontWeight: '500', color: '#666' }}>设备名称</label> </Row>
<span style={{ marginLeft: 8, color: '#333' }}>{selectedDevice.name || '-'}</span> </Card>
</div>
<div> {/* 维保信息卡片 */}
<label style={{ fontWeight: '500', color: '#666' }}>设备类型</label> <Card
<span style={{ marginLeft: 8 }}> size="small"
{selectedDevice.type ? ( title={<span style={{ fontWeight: 600 }}>维保信息</span>}
<Space> style={{ marginBottom: '16px', borderRadius: '8px' }}
{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 <Row gutter={[24, 16]}>
? getStatusConfig(selectedDevice.status).text || selectedDevice.status <Col span={12}>
: '-'} <div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>购买日期</div>
</span> <div style={{ fontWeight: 500 }}>
</div>
<div>
<label style={{ fontWeight: '500', color: '#666' }}>购买日期</label>
<span style={{ marginLeft: 8, color: '#333' }}>
{selectedDevice.purchaseDate {selectedDevice.purchaseDate
? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN') ? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN')
: '-'} : '-'}
</span>
</div> </div>
<div> </Col>
<label style={{ fontWeight: '500', color: '#666' }}>保修到期</label> <Col span={12}>
<span <div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>保修到期</div>
style={{ <div style={{
marginLeft: 8, fontWeight: selectedDevice.warrantyExpiry && new Date(selectedDevice.warrantyExpiry) < new Date() ? 600 : 500,
color: color: selectedDevice.warrantyExpiry && new Date(selectedDevice.warrantyExpiry) < new Date() ? '#d93025' : '#333'
selectedDevice.warrantyExpiry && }}>
new Date(selectedDevice.warrantyExpiry) < new Date()
? '#d93025'
: '#333',
fontWeight:
selectedDevice.warrantyExpiry &&
new Date(selectedDevice.warrantyExpiry) < new Date()
? '600'
: 'normal',
}}
>
{selectedDevice.warrantyExpiry {selectedDevice.warrantyExpiry
? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN') ? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN')
: '-'} : '-'}
</span>
</div>
</div> </div>
</Col>
</Row>
</Card>
{/* 描述信息 */}
{selectedDevice.description && ( {selectedDevice.description && (
<div style={{ marginTop: '16px' }}> <Card
<label style={{ fontWeight: '500', color: '#666' }}>描述</label> size="small"
<div title={<span style={{ fontWeight: 600 }}>描述</span>}
style={{ style={{ marginBottom: '16px', borderRadius: '8px' }}
marginTop: '8px',
padding: '12px',
backgroundColor: '#fafafa',
borderRadius: '8px',
color: '#333',
}}
> >
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>
{selectedDevice.description} {selectedDevice.description}
</div> </div>
</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>
+410 -229
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,167 +128,294 @@ const Login = () => {
} }
}; };
const containerStyle = { // 左侧宣传区域组件
minHeight: '100vh', const LeftPanel = () => (
<div
style={{
height: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', padding: '60px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)', color: '#fff',
padding: '24px',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
}; }}
>
const backgroundDecorationStyle = { {/* 背景装饰 */}
<div
style={{
position: 'absolute', position: 'absolute',
width: '600px',
height: '600px',
borderRadius: '50%', borderRadius: '50%',
filter: 'blur(80px)', background: 'rgba(255,255,255,0.1)',
opacity: '0.3', top: '-200px',
}; left: '-200px',
filter: 'blur(60px)',
}}
/>
<div
style={{
position: 'absolute',
width: '400px',
height: '400px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.08)',
bottom: '-100px',
right: '-100px',
filter: 'blur(40px)',
}}
/>
const cardStyle = { <div style={{ position: 'relative', zIndex: 1 }}>
width: '100%', <div
maxWidth: isFirstUser ? 480 : 420, style={{
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', width: '80px',
height: '80px', height: '80px',
borderRadius: '20px', 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>
<Title
level={1}
style={{
color: '#fff',
fontSize: '48px',
fontWeight: 700,
marginBottom: '24px',
lineHeight: 1.2,
}}
>
IDC设备
<br />
管理系统
</Title>
<Paragraph
style={{
color: 'rgba(255,255,255,0.85)',
fontSize: '18px',
lineHeight: 1.8,
maxWidth: '480px',
marginBottom: '48px',
}}
>
专业的数据中心设备管理平台提供机房机柜设备的全生命周期管理
助力企业实现高效的IT资产管理
</Paragraph>
<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>
);
// 获取标题和副标题
const getHeaderContent = () => {
if (isFirstUser) {
return {
title: '创建管理员账户',
subtitle: '首次使用,请创建系统管理员账户',
};
}
if (unlockMode) {
return {
title: '账户解锁',
subtitle: '输入账户信息以解锁账户',
};
}
if (registerMode) {
return {
title: '注册新账户',
subtitle: '填写信息完成账户注册',
};
}
return {
title: '欢迎回来',
subtitle: '请登录您的账户以继续',
};
};
const headerContent = getHeaderContent();
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>
{/* 右侧登录区域 */}
<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"
/>
<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>
)}
{/* 头部 */}
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div
style={{
width: '64px',
height: '64px',
borderRadius: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
margin: '0 auto 20px', margin: '0 auto 20px',
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.4)', boxShadow: '0 8px 24px rgba(102, 126, 234, 0.35)',
};
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
style={{
...backgroundDecorationStyle,
width: '400px',
height: '400px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
top: '-100px',
right: '-100px',
}} }}
/> >
<div <CloudServerOutlined style={{ fontSize: '32px', color: '#fff' }} />
style={{
...backgroundDecorationStyle,
width: '300px',
height: '300px',
background: 'linear-gradient(135deg, #764ba2 0%, #6B8DD6 100%)',
bottom: '-50px',
left: '-50px',
}}
/>
<Card style={cardStyle}>
<div style={headerStyle}>
<div style={iconContainerStyle}>
<RobotOutlined style={{ fontSize: '40px', color: '#fff' }} />
</div> </div>
<Title level={2} style={titleStyle}> <Title
{isFirstUser ? '创建管理员账户' : unlockMode ? '账户解锁' : 'IDC设备管理系统'} level={3}
style={{
fontSize: '28px',
fontWeight: 700,
color: '#1e293b',
marginBottom: '8px',
}}
>
{headerContent.title}
</Title> </Title>
<Text style={subtitleStyle}> <Text style={{ fontSize: '15px', color: '#64748b' }}>
{isFirstUser {headerContent.subtitle}
? '首次使用,请创建系统管理员账户'
: unlockMode
? '输入账户信息以解锁账户'
: '安全登录您的账户'}
</Text> </Text>
</div> </div>
{/* 首次使用提示 */}
{isFirstUser && ( {isFirstUser && (
<Alert <Alert
message="欢迎使用IDC设备管理系统" message="欢迎使用IDC设备管理系统"
description="您是第一个用户,系统将自动为您分配管理员权限。" description="您是第一个用户,系统将自动为您分配管理员权限。"
type="success" type="success"
showIcon showIcon
style={{ marginBottom: '24px', borderRadius: '8px' }} style={{ marginBottom: '24px', borderRadius: '12px' }}
/> />
)} )}
{/* 解锁模式提示 */}
{unlockMode && (
<Alert
message="账户解锁说明"
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
type="info"
showIcon
style={{ marginBottom: '24px', borderRadius: '12px' }}
/>
)}
{/* 表单 */}
<Form <Form
name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'} name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'}
size="large" size="large"
onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin} onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin}
style={formStyle} layout="vertical"
requiredMark={false}
> >
{registerMode ? ( {registerMode ? (
<> <>
<Row gutter={16}>
<Col span={12}>
<Form.Item <Form.Item
name="username" name="username"
label="用户名"
rules={[ rules={[
{ required: true, message: '请输入用户名' }, { required: true, message: '请输入用户名' },
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' }, { min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
@@ -283,58 +423,76 @@ const Login = () => {
]} ]}
> >
<Input <Input
prefix={<UserOutlined style={inputPrefixStyle} />} prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="用户名" placeholder="请输入用户名"
style={inputStyle} style={{ borderRadius: '12px', height: '48px' }}
/> />
</Form.Item> </Form.Item>
</Col>
<Form.Item name="realName" rules={[{ required: true, message: '请输入真实姓名' }]}> <Col span={12}>
<Form.Item
name="realName"
label="真实姓名"
rules={[{ required: true, message: '请输入真实姓名' }]}
>
<Input <Input
prefix={<SafetyCertificateOutlined style={inputPrefixStyle} />} prefix={<SafetyCertificateOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="真实姓名" placeholder="请输入真实姓名"
style={inputStyle} style={{ borderRadius: '12px', height: '48px' }}
/> />
</Form.Item> </Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item <Form.Item
name="email" name="email"
label="邮箱"
rules={[ rules={[
{ required: true, message: '请输入邮箱' }, { required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' }, { type: 'email', message: '请输入有效的邮箱地址' },
]} ]}
> >
<Input <Input
prefix={<MailOutlined style={inputPrefixStyle} />} prefix={<MailOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="邮箱" placeholder="请输入邮箱"
style={inputStyle} style={{ borderRadius: '12px', height: '48px' }}
/> />
</Form.Item> </Form.Item>
</Col>
<Form.Item name="phone"> <Col span={12}>
<Form.Item name="phone" label="手机号">
<Input <Input
prefix={<PhoneOutlined style={inputPrefixStyle} />} prefix={<PhoneOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="手机号(可选)" placeholder="请输入手机号(可选)"
style={inputStyle} style={{ borderRadius: '12px', height: '48px' }}
/> />
</Form.Item> </Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item <Form.Item
name="password" name="password"
label="密码"
rules={[ rules={[
{ required: true, message: '请输入密码' }, { required: true, message: '请输入密码' },
{ min: 6, message: '密码长度不能少于6个字符' }, { min: 6, message: '密码长度不能少于6个字符' },
]} ]}
> >
<Input.Password <Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />} prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="密码" placeholder="请输入密码"
style={inputStyle} style={{ borderRadius: '12px', height: '48px' }}
/> />
</Form.Item> </Form.Item>
</Col>
<Col span={12}>
<Form.Item <Form.Item
name="confirmPassword" name="confirmPassword"
label="确认密码"
dependencies={['password']} dependencies={['password']}
rules={[ rules={[
{ required: true, message: '请确认密码' }, { required: true, message: '请确认密码' },
@@ -349,125 +507,148 @@ const Login = () => {
]} ]}
> >
<Input.Password <Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />} prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="确认密码" placeholder="请再次输入密码"
style={inputStyle} style={{ borderRadius: '12px', height: '48px' }}
/>
</Form.Item>
</>
) : unlockMode ? (
<>
<Alert
message="账户解锁说明"
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
type="info"
showIcon
style={{ marginBottom: '24px', borderRadius: '8px' }}
/>
<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> </Form.Item>
</Col>
</Row>
</> </>
) : ( ) : (
<> <>
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}> <Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input <Input
prefix={<UserOutlined style={inputPrefixStyle} />} prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="用户名" placeholder="请输入用户名"
style={inputStyle} style={{ borderRadius: '12px', height: '52px' }}
/> />
</Form.Item> </Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}> <Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
style={{ marginBottom: '8px' }}
>
<Input.Password <Input.Password
prefix={<LockOutlined style={inputPrefixStyle} />} prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
placeholder="密码" placeholder="请输入密码"
style={inputStyle} style={{ borderRadius: '12px', height: '52px' }}
/> />
</Form.Item> </Form.Item>
{!registerMode && !unlockMode && (
<div style={{ textAlign: 'right', marginBottom: '24px' }}>
<Button type="link" style={{ color: '#667eea', padding: 0 }}>
忘记密码
</Button>
</div>
)}
</> </>
)} )}
<Form.Item style={{ marginBottom: '16px', marginTop: '24px' }}> <Form.Item style={{ marginTop: '32px', marginBottom: '16px' }}>
<Button type="primary" htmlType="submit" loading={loading} style={submitButtonStyle}> <Button
{registerMode ? '立即注册' : unlockMode ? '解 锁' : '登 录'} 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> </Button>
</Form.Item> </Form.Item>
</Form> </Form>
{/* 底部切换 */}
{!isFirstUser && ( {!isFirstUser && (
<div style={footerStyle}> <div style={{ textAlign: 'center', marginTop: '24px' }}>
<Divider plain> {!unlockMode && !registerMode && (
<Text style={{ color: '#8c8c8c', fontSize: '12px' }}>其他方式</Text> <Space split={<Divider type="vertical" />} size="large">
</Divider>
<Space split={<Divider type="vertical" />}>
{unlockMode ? (
<>
<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>
)}
{(registerMode || unlockMode) && (
<Text style={{ color: '#64748b' }}>
已有账户{' '}
<Button
type="link"
onClick={() => {
setRegisterMode(false);
setUnlockMode(false);
}}
style={{ color: '#667eea', fontWeight: 600, padding: 0 }}
>
立即登录
</Button>
</Text>
)}
</div> </div>
)} )}
</Card> </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> </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>
); );
}; };
+313 -129
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,81 +379,134 @@ 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>
{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] }; const settingData = { ...settings[key] };
// 确保特定字段使用正确的类型
if (key === 'timezone' || key === 'date_format') { if (key === 'timezone' || key === 'date_format') {
settingData.type = 'select'; 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>
);
};
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') { if (key === 'primary_color' || key === 'secondary_color') {
settingData.type = 'select'; settingData.type = 'select';
} }
return renderFormItem(key, settingData); return renderFormItem(key, settingData);
})} })}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving}>
保存设置
</Button>
<Button onClick={() => fetchSettings()}>重置表单</Button>
</Space>
</Form.Item>
</Form>
</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 = () => {
return (
<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 renderAboutPage = () => {
const aboutKeys = [ const aboutKeys = [
@@ -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>
<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()}
> >
保存信息 <Row gutter={[24, 24]} align="middle">
</Button> <Col>
<Button onClick={() => fetchSettings()}>重置</Button> <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> </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>
<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} {systemInfo.statistics?.devices || 0}
</Descriptions.Item> </div>
<Descriptions.Item label="机柜总数"> <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} {systemInfo.statistics?.racks || 0}
</Descriptions.Item> </div>
<Descriptions.Item label="机房总数"> <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} {systemInfo.statistics?.rooms || 0}
</Descriptions.Item> </div>
<Descriptions.Item label="用户总数"> <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} {systemInfo.statistics?.users || 0}
</Descriptions.Item> </div>
</Descriptions> <div style={{ color: '#666', fontSize: 12 }}>用户总数</div>
<Divider /> </Card>
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small"> </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>
); );
}; };
return ( const tabItems = [
<div style={{ padding: 24 }}> {
<Tabs activeKey={activeTab} onChange={setActiveTab}> key: 'general',
<TabPane label: (
tab={
<span> <span>
<GlobalOutlined /> 全局配置 <GlobalOutlined /> 全局配置
</span> </span>
} ),
key="general" children: renderGeneralSettings(),
> },
{renderGeneralSettings()} {
</TabPane> key: 'appearance',
<TabPane label: (
tab={
<span> <span>
<BgColorsOutlined /> 外观设置 <BgColorsOutlined /> 外观设置
</span> </span>
} ),
key="appearance" children: renderAppearanceSettings(),
> },
{renderAppearanceSettings()} {
</TabPane> key: 'about',
<TabPane label: (
tab={
<span> <span>
<InfoCircleOutlined /> 关于 <InfoCircleOutlined /> 关于系统
</span> </span>
),
children: renderAboutPage(),
},
];
return (
<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>
} }
key="about" style={{ borderRadius: 12 }}
bodyStyle={{ padding: 0 }}
> >
{renderAboutPage()} <Tabs
</TabPane> activeKey={activeTab}
</Tabs> onChange={setActiveTab}
items={tabItems}
style={{ padding: '0 24px 24px' }}
tabBarStyle={{ marginBottom: 24 }}
/>
</Card>
</div> </div>
); );
}; };
import { LockOutlined } from '@ant-design/icons';
export default SystemSettings; export default SystemSettings;
+237 -68
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,41 +1169,20 @@ 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 || '-'}
</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 <Tag
color={ color={
selectedTicket.Device.status === 'running' selectedTicket.Device.status === 'running'
@@ -1210,18 +1204,96 @@ function TicketManagement() {
? '离线' ? '离线'
: selectedTicket.Device.status || '-'} : selectedTicket.Device.status || '-'}
</Tag> </Tag>
</Descriptions.Item> </Col>
<Descriptions.Item label="购买日期"> <Col span={8}>
{selectedTicket.Device.purchaseDate <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') ? dayjs(selectedTicket.Device.purchaseDate).format('YYYY-MM-DD')
: '-'} : '-'}</div>
</Descriptions.Item> </Col>
<Descriptions.Item label="保修到期"> <Col span={12}>
{selectedTicket.Device.warrantyDate <div style={{ color: '#666', fontSize: 12, marginBottom: 4 }}>保修到期</div>
? dayjs(selectedTicket.Device.warrantyDate).format('YYYY-MM-DD') <div>{selectedTicket.Device.warrantyExpiry
: '-'} ? dayjs(selectedTicket.Device.warrantyExpiry).format('YYYY-MM-DD')
</Descriptions.Item> : '-'}</div>
</Descriptions> </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' }}>
<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} key={index}
label={dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')} style={{
color={ position: 'relative',
record.operationType === 'create' paddingLeft: '48px',
? 'green' marginBottom: '24px',
: record.operationType === 'complete' }}
? 'blue' >
: record.operationType === 'close' {/* 时间点圆点 */}
? 'gray' <div
: 'orange' 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',
}}
> >
<div> {record.operationDescription}
<strong>{record.operationType}</strong> </div>
)}
</Card>
</div>
);
})}
</div>
)}
</div> </div>
<div>操作人: {record.operatorName || '-'}</div>
<div>内容: {record.operationDescription || '-'}</div>
</Timeline.Item>
))}
{operationRecords.length === 0 && <p style={{ color: '#888' }}>暂无操作记录</p>}
</Timeline>
</TabPane> </TabPane>
</Tabs> </Tabs>
)} )}