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