feat(工单/设备): 优化设备详情展示和工单操作记录样式
refactor(系统设置): 重构系统设置页面布局和样式 style(前端): 统一设备管理相关组件的卡片样式和布局
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user