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