feat(设备管理): 重构设备管理页面并优化初始化逻辑
This commit is contained in:
@@ -160,21 +160,20 @@ async function initDeviceFields() {
|
||||
console.log('开始初始化设备字段配置...');
|
||||
// 注意:数据库连接和表结构同步已在 server.js 中完成,这里直接初始化数据
|
||||
|
||||
// 批量创建默认字段
|
||||
// 批量创建默认字段(只创建不存在的字段,保留用户自定义配置)
|
||||
for (const field of defaultDeviceFields) {
|
||||
// 检查字段是否已存在
|
||||
const existingField = await DeviceField.findOne({
|
||||
where: { fieldName: field.fieldName }
|
||||
});
|
||||
|
||||
if (existingField) {
|
||||
// 更新已有字段
|
||||
await existingField.update(field);
|
||||
console.log(`更新字段: ${field.displayName}`);
|
||||
} else {
|
||||
// 创建新字段
|
||||
if (!existingField) {
|
||||
// 只创建新字段,不更新已存在的字段
|
||||
await DeviceField.create(field);
|
||||
console.log(`创建字段: ${field.displayName}`);
|
||||
} else {
|
||||
// 已存在的字段跳过,保留用户自定义配置
|
||||
console.log(`跳过已存在字段: ${field.displayName}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+59
-19
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Op } = require('sequelize');
|
||||
const { sequelize } = require('../db'); // Import sequelize for transactions
|
||||
const { sequelize, dbDialect } = require('../db'); // Import sequelize and dbDialect for transactions
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const csv = require('csv-parser');
|
||||
@@ -30,39 +30,74 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => {
|
||||
try {
|
||||
const { keyword, status, type, rackId, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
|
||||
// 关键词搜索(所有文本字段)
|
||||
|
||||
// 关键词搜索(所有文本字段 + 自定义字段)
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ deviceId: { [Op.like]: `%${keyword}%` } },
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ type: { [Op.like]: `%${keyword}%` } },
|
||||
{ model: { [Op.like]: `%${keyword}%` } },
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||
{ position: { [Op.like]: `%${keyword}%` } },
|
||||
{ ipAddress: { [Op.like]: `%${keyword}%` } },
|
||||
{ description: { [Op.like]: `%${keyword}%` } }
|
||||
console.log('搜索关键词:', keyword);
|
||||
console.log('数据库类型:', dbDialect);
|
||||
|
||||
// 转义关键词中的特殊字符,防止SQL注入
|
||||
const escapedKeyword = keyword.replace(/'/g, "''");
|
||||
|
||||
// 基础字段搜索条件(只包含文本类型字段)
|
||||
const searchConditions = [
|
||||
{ deviceId: { [Op.like]: `%${escapedKeyword}%` } },
|
||||
{ name: { [Op.like]: `%${escapedKeyword}%` } },
|
||||
{ type: { [Op.like]: `%${escapedKeyword}%` } },
|
||||
{ model: { [Op.like]: `%${escapedKeyword}%` } },
|
||||
{ serialNumber: { [Op.like]: `%${escapedKeyword}%` } },
|
||||
{ ipAddress: { [Op.like]: `%${escapedKeyword}%` } },
|
||||
{ description: { [Op.like]: `%${escapedKeyword}%` } }
|
||||
];
|
||||
|
||||
// 动态获取文本类型的自定义字段
|
||||
const customFields = await DeviceField.findAll({
|
||||
where: {
|
||||
isSystem: false,
|
||||
fieldType: { [Op.in]: ['string', 'textarea'] }
|
||||
}
|
||||
});
|
||||
|
||||
console.log('找到的自定义字段:', customFields.map(f => f.fieldName));
|
||||
|
||||
// 构建自定义字段搜索条件(使用原始SQL,兼容SQLite和MySQL)
|
||||
if (customFields.length > 0) {
|
||||
// 使用原始SQL查询JSON字段
|
||||
const jsonConditions = customFields.map(field => {
|
||||
const fieldName = field.fieldName;
|
||||
// 使用 sequelize.literal 构建原始SQL条件
|
||||
if (dbDialect === 'mysql') {
|
||||
return sequelize.literal(`JSON_EXTRACT(customFields, '$."${fieldName}"') LIKE '%${escapedKeyword}%'`);
|
||||
} else {
|
||||
return sequelize.literal(`json_extract(customFields, '$.${fieldName}') LIKE '%${escapedKeyword}%'`);
|
||||
}
|
||||
});
|
||||
searchConditions.push(...jsonConditions);
|
||||
}
|
||||
|
||||
// 合并所有搜索条件
|
||||
where[Op.or] = searchConditions;
|
||||
console.log('搜索条件数量:', searchConditions.length);
|
||||
}
|
||||
|
||||
|
||||
// 状态筛选
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
|
||||
// 分类筛选
|
||||
if (type && type !== 'all') {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
|
||||
// 机柜筛选(用于机柜可视化功能)
|
||||
if (rackId) {
|
||||
where.rackId = rackId;
|
||||
}
|
||||
|
||||
|
||||
// 执行查询
|
||||
const { count, rows } = await Device.findAndCountAll({
|
||||
where,
|
||||
@@ -77,7 +112,7 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => {
|
||||
offset,
|
||||
limit: parseInt(pageSize)
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
devices: rows,
|
||||
@@ -85,7 +120,12 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => {
|
||||
pageSize: parseInt(pageSize)
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
console.error('搜索设备失败:', error);
|
||||
console.error('错误详情:', error.message);
|
||||
if (error.sql) {
|
||||
console.error('SQL:', error.sql);
|
||||
}
|
||||
res.status(500).json({ error: error.message, sql: error.sql });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 主题配置文件
|
||||
* 集中管理应用的颜色、阴影、间距等设计令牌
|
||||
*/
|
||||
|
||||
export const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: '#667eea',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
light: '#8b9ff0',
|
||||
dark: '#4f5db8'
|
||||
},
|
||||
success: {
|
||||
main: '#10b981',
|
||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
light: '#34d399',
|
||||
dark: '#047857'
|
||||
},
|
||||
warning: {
|
||||
main: '#f59e0b',
|
||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
light: '#fbbf24',
|
||||
dark: '#b45309'
|
||||
},
|
||||
error: {
|
||||
main: '#ef4444',
|
||||
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
light: '#f87171',
|
||||
dark: '#b91c1c'
|
||||
},
|
||||
text: {
|
||||
primary: '#1e293b',
|
||||
secondary: '#64748b',
|
||||
tertiary: '#94a3b8',
|
||||
inverse: '#ffffff'
|
||||
},
|
||||
background: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#f8fafc',
|
||||
tertiary: '#f1f5f9',
|
||||
dark: '#1e293b'
|
||||
},
|
||||
border: {
|
||||
light: '#e2e8f0',
|
||||
medium: '#cbd5e1',
|
||||
dark: '#94a3b8'
|
||||
},
|
||||
device: {
|
||||
server: '#3b82f6',
|
||||
switch: '#22c55e',
|
||||
router: '#f59e0b',
|
||||
storage: '#8b5cf6',
|
||||
other: '#64748b'
|
||||
},
|
||||
status: {
|
||||
normal: '#10b981',
|
||||
running: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
fault: '#ef4444',
|
||||
offline: '#6b7280',
|
||||
maintenance: '#3b82f6'
|
||||
}
|
||||
},
|
||||
shadows: {
|
||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
||||
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
||||
glow: '0 0 20px rgba(102, 126, 234, 0.3)'
|
||||
},
|
||||
borderRadius: {
|
||||
small: '6px',
|
||||
medium: '10px',
|
||||
large: '16px',
|
||||
xl: '24px',
|
||||
round: '50%'
|
||||
},
|
||||
transitions: {
|
||||
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
},
|
||||
spacing: {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px'
|
||||
}
|
||||
};
|
||||
|
||||
export default designTokens;
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 设备管理页面常量配置
|
||||
* 集中管理分页、延迟、字段等配置
|
||||
*/
|
||||
|
||||
// 分页配置
|
||||
export const PAGINATION_CONFIG = {
|
||||
// 默认每页条数
|
||||
defaultPageSize: 10,
|
||||
// 可选每页条数
|
||||
pageSizeOptions: ['10', '20', '30', '50', '100'],
|
||||
// 显示快速跳转
|
||||
showSizeChanger: true,
|
||||
// 显示总数
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
};
|
||||
|
||||
// 搜索防抖延迟(毫秒)
|
||||
export const DEBOUNCE_DELAY = 300;
|
||||
|
||||
// 表格滚动配置
|
||||
export const TABLE_SCROLL_CONFIG = {
|
||||
x: 'max-content',
|
||||
y: 'calc(100vh - 400px)'
|
||||
};
|
||||
|
||||
// 默认设备字段配置
|
||||
export const DEFAULT_DEVICE_FIELDS = [
|
||||
{
|
||||
fieldName: 'deviceId',
|
||||
displayName: '设备ID',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: false
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
displayName: '设备名称',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
displayName: '设备类型',
|
||||
fieldType: 'select',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: true,
|
||||
options: [
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他设备' }
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldName: 'model',
|
||||
displayName: '设备型号',
|
||||
fieldType: 'text',
|
||||
required: false,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'serialNumber',
|
||||
displayName: '序列号',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'rackId',
|
||||
displayName: '所在机柜',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'position',
|
||||
displayName: '位置(U)',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'height',
|
||||
displayName: '高度(U)',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'powerConsumption',
|
||||
displayName: '功率(W)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'ipAddress',
|
||||
displayName: 'IP地址',
|
||||
fieldType: 'text',
|
||||
required: false,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
displayName: '状态',
|
||||
fieldType: 'select',
|
||||
required: true,
|
||||
visible: true,
|
||||
editable: true,
|
||||
options: [
|
||||
{ value: 'running', label: '运行中' },
|
||||
{ value: 'maintenance', label: '维护中' },
|
||||
{ value: 'offline', label: '离线' },
|
||||
{ value: 'fault', label: '故障' }
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldName: 'purchaseDate',
|
||||
displayName: '购买日期',
|
||||
fieldType: 'date',
|
||||
required: false,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'warrantyExpiry',
|
||||
displayName: '保修日期',
|
||||
fieldType: 'date',
|
||||
required: false,
|
||||
visible: true,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
displayName: '备注',
|
||||
fieldType: 'textarea',
|
||||
required: false,
|
||||
visible: true,
|
||||
editable: true
|
||||
}
|
||||
];
|
||||
|
||||
// 基础字段名称列表(用于导入导出时排除自定义字段)
|
||||
export const BASE_FIELD_NAMES = [
|
||||
'deviceId',
|
||||
'name',
|
||||
'type',
|
||||
'model',
|
||||
'serialNumber',
|
||||
'rackId',
|
||||
'position',
|
||||
'height',
|
||||
'powerConsumption',
|
||||
'ipAddress',
|
||||
'status',
|
||||
'purchaseDate',
|
||||
'warrantyExpiry',
|
||||
'description'
|
||||
];
|
||||
|
||||
// 系统字段列表(不可编辑)
|
||||
export const SYSTEM_FIELDS = ['createdAt', 'updatedAt', 'Rack', 'Room', 'customFields'];
|
||||
|
||||
// 固定字段列表(表格中必须显示的字段)
|
||||
export const FIXED_FIELDS = ['name', 'type', 'status', 'rackId'];
|
||||
|
||||
// 导入配置
|
||||
export const IMPORT_CONFIG = {
|
||||
// 支持的文件类型
|
||||
acceptedFileTypes: '.csv,.xlsx,.xls',
|
||||
// 最大文件大小(MB)
|
||||
maxFileSize: 10,
|
||||
// 单次最大导入条数
|
||||
maxImportCount: 5000,
|
||||
// 编码格式
|
||||
encoding: 'gbk'
|
||||
};
|
||||
|
||||
// 导出配置
|
||||
export const EXPORT_CONFIG = {
|
||||
// 默认文件名前缀
|
||||
fileNamePrefix: '设备列表',
|
||||
// 日期格式
|
||||
dateFormat: 'YYYY-MM-DD_HH-mm-ss',
|
||||
// 支持的导出格式
|
||||
formats: ['xlsx', 'csv']
|
||||
};
|
||||
|
||||
// 模态框配置
|
||||
export const MODAL_CONFIG = {
|
||||
// 添加设备模态框宽度
|
||||
addModalWidth: 900,
|
||||
// 编辑设备模态框宽度
|
||||
editModalWidth: 900,
|
||||
// 详情模态框宽度
|
||||
detailModalWidth: 700,
|
||||
// 导入模态框宽度
|
||||
importModalWidth: 600,
|
||||
// 导出模态框宽度
|
||||
exportModalWidth: 500
|
||||
};
|
||||
|
||||
// 统计卡片配置
|
||||
export const STATS_CONFIG = {
|
||||
// 显示的运行中设备数量上限(超过显示为 99+)
|
||||
maxRunningDisplay: 99,
|
||||
// 显示的维护中设备数量上限
|
||||
maxMaintenanceDisplay: 99,
|
||||
// 显示的故障设备数量上限
|
||||
maxFaultDisplay: 99
|
||||
};
|
||||
|
||||
// 设备类型选项(用于筛选)
|
||||
export const DEVICE_TYPE_OPTIONS = [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'server', label: '服务器' },
|
||||
{ value: 'switch', label: '交换机' },
|
||||
{ value: 'router', label: '路由器' },
|
||||
{ value: 'storage', label: '存储设备' },
|
||||
{ value: 'other', label: '其他设备' }
|
||||
];
|
||||
|
||||
// 设备状态选项(用于筛选)
|
||||
export const DEVICE_STATUS_OPTIONS = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'running', label: '运行中' },
|
||||
{ value: 'maintenance', label: '维护中' },
|
||||
{ value: 'offline', label: '离线' },
|
||||
{ value: 'fault', label: '故障' }
|
||||
];
|
||||
|
||||
// 表格列宽配置
|
||||
export const COLUMN_WIDTH_CONFIG = {
|
||||
deviceId: 100,
|
||||
name: 150,
|
||||
type: 100,
|
||||
model: 120,
|
||||
serialNumber: 150,
|
||||
rackId: 150,
|
||||
position: 80,
|
||||
height: 80,
|
||||
powerConsumption: 100,
|
||||
ipAddress: 120,
|
||||
status: 100,
|
||||
purchaseDate: 110,
|
||||
warrantyExpiry: 110,
|
||||
description: 200,
|
||||
action: 150
|
||||
};
|
||||
|
||||
// 空状态配置
|
||||
export const EMPTY_STATE_CONFIG = {
|
||||
description: '暂无设备数据',
|
||||
image: 'https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg'
|
||||
};
|
||||
|
||||
// 操作按钮配置
|
||||
export const ACTION_BUTTON_CONFIG = {
|
||||
// 批量操作阈值(超过此数量显示确认对话框)
|
||||
batchConfirmThreshold: 10,
|
||||
// 批量删除确认消息
|
||||
batchDeleteConfirmMessage: (count) => `确定要删除选中的 ${count} 个设备吗?此操作不可恢复。`,
|
||||
// 单个删除确认消息
|
||||
singleDeleteConfirmMessage: (name) => `确定要删除设备 "${name}" 吗?此操作不可恢复。`
|
||||
};
|
||||
@@ -3,112 +3,73 @@ import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, S
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, SettingOutlined, UndoOutlined, CloudServerOutlined, SafetyOutlined, DatabaseOutlined, AppstoreOutlined, MoreOutlined, ReloadOutlined, ExportOutlined, FileExcelOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
import { designTokens } from '../config/theme';
|
||||
import {
|
||||
PAGINATION_CONFIG,
|
||||
DEBOUNCE_DELAY,
|
||||
TABLE_SCROLL_CONFIG,
|
||||
DEFAULT_DEVICE_FIELDS,
|
||||
BASE_FIELD_NAMES,
|
||||
SYSTEM_FIELDS,
|
||||
FIXED_FIELDS,
|
||||
IMPORT_CONFIG,
|
||||
EXPORT_CONFIG,
|
||||
MODAL_CONFIG,
|
||||
DEVICE_TYPE_OPTIONS,
|
||||
DEVICE_STATUS_OPTIONS,
|
||||
COLUMN_WIDTH_CONFIG,
|
||||
EMPTY_STATE_CONFIG
|
||||
} from '../constants/deviceManagementConstants';
|
||||
import {
|
||||
pageContainerStyle,
|
||||
headerStyle,
|
||||
titleRowStyle,
|
||||
titleSectionStyle,
|
||||
titleIconStyle,
|
||||
titleTextStyle,
|
||||
pageTitleStyle,
|
||||
pageSubtitleStyle,
|
||||
primaryActionStyle,
|
||||
secondaryActionStyle,
|
||||
statsRowStyle,
|
||||
statCardStyle,
|
||||
statValueStyle,
|
||||
statLabelStyle,
|
||||
statCardRunningStyle,
|
||||
statCardMaintenanceStyle,
|
||||
statCardFaultStyle,
|
||||
cardStyle,
|
||||
filterCardStyle,
|
||||
modalHeaderStyle,
|
||||
tableStyles,
|
||||
searchInputStyle,
|
||||
selectStyle,
|
||||
refreshButtonStyle,
|
||||
searchButtonStyle,
|
||||
resetButtonStyle,
|
||||
importModalStyles,
|
||||
detailModalStyles,
|
||||
exportModalStyles,
|
||||
generateGlobalStyles
|
||||
} from '../styles/deviceManagementStyles';
|
||||
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: '#667eea',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
light: '#8b9ff0',
|
||||
dark: '#4f5db8'
|
||||
},
|
||||
success: {
|
||||
main: '#10b981',
|
||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
light: '#34d399',
|
||||
dark: '#047857'
|
||||
},
|
||||
warning: {
|
||||
main: '#f59e0b',
|
||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
light: '#fbbf24',
|
||||
dark: '#b45309'
|
||||
},
|
||||
error: {
|
||||
main: '#ef4444',
|
||||
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
light: '#f87171',
|
||||
dark: '#b91c1c'
|
||||
},
|
||||
text: {
|
||||
primary: '#1e293b',
|
||||
secondary: '#64748b',
|
||||
tertiary: '#94a3b8',
|
||||
inverse: '#ffffff'
|
||||
},
|
||||
background: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#f8fafc',
|
||||
tertiary: '#f1f5f9',
|
||||
dark: '#1e293b'
|
||||
},
|
||||
border: {
|
||||
light: '#e2e8f0',
|
||||
medium: '#cbd5e1',
|
||||
dark: '#94a3b8'
|
||||
},
|
||||
device: {
|
||||
server: '#3b82f6',
|
||||
switch: '#22c55e',
|
||||
router: '#f59e0b',
|
||||
storage: '#8b5cf6',
|
||||
other: '#64748b'
|
||||
},
|
||||
status: {
|
||||
normal: '#10b981',
|
||||
running: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
fault: '#ef4444',
|
||||
offline: '#6b7280',
|
||||
maintenance: '#3b82f6'
|
||||
}
|
||||
},
|
||||
shadows: {
|
||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
||||
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
||||
glow: '0 0 20px rgba(102, 126, 234, 0.3)'
|
||||
},
|
||||
borderRadius: {
|
||||
small: '6px',
|
||||
medium: '10px',
|
||||
large: '16px',
|
||||
xl: '24px',
|
||||
round: '50%'
|
||||
},
|
||||
transitions: {
|
||||
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
},
|
||||
spacing: {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px'
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖 Hook
|
||||
function useDebounce(value, delay) {
|
||||
function useDebounce(value, delay = DEBOUNCE_DELAY) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
@@ -165,25 +126,8 @@ const formatDate = (date, fieldName) => {
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
// 默认设备字段配置
|
||||
const defaultDeviceFields = [
|
||||
{ fieldName: 'deviceId', displayName: '设备ID', fieldType: 'string', required: false, order: 1, visible: false },
|
||||
{ fieldName: 'name', displayName: '设备名称', fieldType: 'string', required: true, order: 2, visible: true },
|
||||
{ fieldName: 'type', displayName: '设备类型', fieldType: 'select', required: true, order: 3, visible: true,
|
||||
options: [{ value: 'server', label: '服务器' }, { value: 'switch', label: '交换机' }, { value: 'router', label: '路由器' }, { value: 'storage', label: '存储设备' }, { value: 'other', label: '其他设备' }] },
|
||||
{ fieldName: 'model', displayName: '型号', fieldType: 'string', required: true, order: 4, visible: true },
|
||||
{ fieldName: 'serialNumber', displayName: '序列号', fieldType: 'string', required: true, order: 5, visible: true },
|
||||
{ fieldName: 'rackId', displayName: '所在机柜', fieldType: 'select', required: true, order: 6, visible: true },
|
||||
{ fieldName: 'position', displayName: '位置(U)', fieldType: 'number', required: true, order: 7, visible: true },
|
||||
{ fieldName: 'height', displayName: '高度(U)', fieldType: 'number', required: true, order: 8, visible: true },
|
||||
{ fieldName: 'powerConsumption', displayName: '功率(W)', fieldType: 'number', required: true, order: 9, visible: true },
|
||||
{ fieldName: 'status', displayName: '状态', fieldType: 'select', required: true, order: 10, visible: true,
|
||||
options: [{ value: 'running', label: '运行中' }, { value: 'maintenance', label: '维护中' }, { value: 'offline', label: '离线' }, { value: 'fault', label: '故障' }] },
|
||||
{ fieldName: 'purchaseDate', displayName: '购买日期', fieldType: 'date', required: false, order: 11, visible: true },
|
||||
{ fieldName: 'warrantyExpiry', displayName: '保修到期', fieldType: 'date', required: false, order: 12, visible: true },
|
||||
{ fieldName: 'ipAddress', displayName: 'IP地址', fieldType: 'string', required: false, order: 13, visible: true },
|
||||
{ fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, order: 14, visible: true }
|
||||
];
|
||||
// 使用从常量文件导入的默认设备字段配置
|
||||
const defaultDeviceFields = DEFAULT_DEVICE_FIELDS;
|
||||
|
||||
// 简单的可调整列宽的表头组件
|
||||
const ResizableTitle = (props) => {
|
||||
@@ -263,14 +207,14 @@ function DeviceManagement() {
|
||||
const [status, setStatus] = useState('all');
|
||||
const [type, setType] = useState('all');
|
||||
const [searchForm] = Form.useForm();
|
||||
// 分页状态
|
||||
// 分页状态 - 使用常量配置
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
pageSize: PAGINATION_CONFIG.defaultPageSize,
|
||||
total: 0,
|
||||
pageSizeOptions: ['10', '20', '30', '50', '100'],
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
pageSizeOptions: PAGINATION_CONFIG.pageSizeOptions,
|
||||
showSizeChanger: PAGINATION_CONFIG.showSizeChanger,
|
||||
showTotal: PAGINATION_CONFIG.showTotal
|
||||
});
|
||||
|
||||
// 设备字段配置
|
||||
@@ -312,8 +256,8 @@ function DeviceManagement() {
|
||||
// 列宽状态
|
||||
const [columnWidths, setColumnWidths] = useState({});
|
||||
|
||||
// 防抖搜索关键词
|
||||
const debouncedKeyword = useDebounce(keyword, 300);
|
||||
// 防抖搜索关键词 - 使用常量配置的延迟时间
|
||||
const debouncedKeyword = useDebounce(keyword, DEBOUNCE_DELAY);
|
||||
|
||||
// 使用 useMemo 缓存筛选后的设备数据(现在直接使用 allDevices,因为后端已经处理了筛选)
|
||||
const filteredDevicesMemo = useMemo(() => {
|
||||
@@ -1149,360 +1093,11 @@ function DeviceManagement() {
|
||||
message.success('字段配置已重置为默认值');
|
||||
};
|
||||
|
||||
const pageContainerStyle = {
|
||||
minHeight: '100vh',
|
||||
background: designTokens.colors.background.secondary,
|
||||
padding: designTokens.spacing.lg
|
||||
};
|
||||
|
||||
const headerStyle = {
|
||||
marginBottom: designTokens.spacing.lg
|
||||
};
|
||||
|
||||
const titleRowStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: designTokens.spacing.lg,
|
||||
flexWrap: 'wrap',
|
||||
gap: designTokens.spacing.md
|
||||
};
|
||||
|
||||
const titleSectionStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.md
|
||||
};
|
||||
|
||||
const titleIconStyle = {
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: designTokens.shadows.medium
|
||||
};
|
||||
|
||||
const titleTextStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px'
|
||||
};
|
||||
|
||||
const pageTitleStyle = {
|
||||
fontSize: '22px',
|
||||
fontWeight: '700',
|
||||
margin: 0,
|
||||
color: designTokens.colors.text.primary,
|
||||
lineHeight: 1.2
|
||||
};
|
||||
|
||||
const pageSubtitleStyle = {
|
||||
fontSize: '13px',
|
||||
color: designTokens.colors.text.secondary,
|
||||
margin: 0
|
||||
};
|
||||
|
||||
const actionButtonStyle = {
|
||||
height: '36px',
|
||||
borderRadius: designTokens.borderRadius.small,
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
};
|
||||
|
||||
const primaryActionStyle = {
|
||||
...actionButtonStyle,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
border: 'none',
|
||||
color: '#ffffff !important',
|
||||
boxShadow: designTokens.shadows.small
|
||||
};
|
||||
|
||||
const secondaryActionStyle = {
|
||||
...actionButtonStyle,
|
||||
background: designTokens.colors.background.primary,
|
||||
border: `1px solid ${designTokens.colors.border.light}`,
|
||||
color: designTokens.colors.text.primary
|
||||
};
|
||||
|
||||
const dangerActionStyle = {
|
||||
...actionButtonStyle,
|
||||
background: designTokens.colors.error.main,
|
||||
border: 'none',
|
||||
color: '#ffffff'
|
||||
};
|
||||
|
||||
const primaryButtonStyle = {
|
||||
height: '40px',
|
||||
borderRadius: designTokens.borderRadius.small,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
boxShadow: designTokens.shadows.small,
|
||||
fontWeight: '500',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
};
|
||||
|
||||
const statsRowStyle = {
|
||||
display: 'flex',
|
||||
gap: designTokens.spacing.md,
|
||||
marginBottom: designTokens.spacing.lg,
|
||||
flexWrap: 'wrap'
|
||||
};
|
||||
|
||||
const statCardStyle = {
|
||||
flex: 1,
|
||||
minWidth: '140px',
|
||||
maxWidth: '200px',
|
||||
padding: `${designTokens.spacing.md}px ${designTokens.spacing.lg}px`,
|
||||
background: designTokens.colors.background.primary,
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
border: `1px solid ${designTokens.colors.border.light}`,
|
||||
boxShadow: designTokens.shadows.small,
|
||||
transition: `all ${designTokens.transitions.fast}`
|
||||
};
|
||||
|
||||
const statValueStyle = {
|
||||
fontSize: '24px',
|
||||
fontWeight: '700',
|
||||
color: designTokens.colors.text.primary,
|
||||
lineHeight: 1.2
|
||||
};
|
||||
|
||||
const statLabelStyle = {
|
||||
fontSize: '12px',
|
||||
color: designTokens.colors.text.secondary,
|
||||
marginTop: '4px'
|
||||
};
|
||||
|
||||
const statCardRunningStyle = {
|
||||
...statCardStyle,
|
||||
borderLeft: `3px solid ${designTokens.colors.success.main}`,
|
||||
background: `${designTokens.colors.success.main}08`
|
||||
};
|
||||
|
||||
const statCardMaintenanceStyle = {
|
||||
...statCardStyle,
|
||||
borderLeft: `3px solid ${designTokens.colors.warning.main}`,
|
||||
background: `${designTokens.colors.warning.main}08`
|
||||
};
|
||||
|
||||
const statCardFaultStyle = {
|
||||
...statCardStyle,
|
||||
borderLeft: `3px solid ${designTokens.colors.error.main}`,
|
||||
background: `${designTokens.colors.error.main}08`
|
||||
};
|
||||
|
||||
const cardStyle = {
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: 'none',
|
||||
boxShadow: designTokens.shadows.medium,
|
||||
overflow: 'hidden',
|
||||
background: designTokens.colors.background.primary
|
||||
};
|
||||
|
||||
const filterCardStyle = {
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
border: 'none',
|
||||
boxShadow: designTokens.shadows.small,
|
||||
background: designTokens.colors.background.primary,
|
||||
marginBottom: designTokens.spacing.lg
|
||||
};
|
||||
|
||||
const modalHeaderStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: designTokens.spacing.sm,
|
||||
fontSize: '18px',
|
||||
fontWeight: '600'
|
||||
};
|
||||
// 样式已从外部样式文件导入,无需在组件内定义
|
||||
|
||||
return (
|
||||
<div style={pageContainerStyle}>
|
||||
<style>{`
|
||||
.device-modal .ant-modal-close {
|
||||
top: 16px;
|
||||
right: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.device-modal .ant-modal-close-x {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.device-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-container {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-content {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-thead {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-thead > tr > th {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 14px 12px !important;
|
||||
background: ${designTokens.colors.background.tertiary} !important;
|
||||
color: ${designTokens.colors.text.primary} !important;
|
||||
border-bottom: 1px solid ${designTokens.colors.border.light} !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody {
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr > td {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
line-height: 1.6 !important;
|
||||
max-width: 250px !important;
|
||||
padding: 12px !important;
|
||||
border-bottom: 1px solid ${designTokens.colors.border.light} !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr > td .ant-typography,
|
||||
.device-table-wrapper .ant-table-tbody > tr > td .ant-typography-expand,
|
||||
.device-table-wrapper .ant-table-tbody > tr > td span {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-cell {
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-even {
|
||||
background-color: ${designTokens.colors.background.primary};
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-odd {
|
||||
background-color: ${designTokens.colors.background.secondary};
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-selected {
|
||||
background-color: ${designTokens.colors.primary.main}15 !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-selected:hover > td {
|
||||
background-color: ${designTokens.colors.primary.main}25 !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr:hover > td {
|
||||
background-color: ${designTokens.colors.background.tertiary} !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-selection-column {
|
||||
position: sticky !important;
|
||||
left: 0 !important;
|
||||
z-index: 2 !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr > td:last-child {
|
||||
min-width: 120px !important;
|
||||
max-width: 150px !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-pagination {
|
||||
margin: 16px 0 !important;
|
||||
flex-wrap: wrap !important;
|
||||
justify-content: center !important;
|
||||
padding: 12px 16px !important;
|
||||
background: ${designTokens.colors.background.primary};
|
||||
border-radius: ${designTokens.borderRadius.medium};
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-pagination-item-active {
|
||||
background: ${designTokens.colors.primary.gradient} !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-pagination-item-active a {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-cell-fix-left,
|
||||
.device-table-wrapper .ant-table-cell-fix-right {
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.device-table-wrapper .ant-table-tbody > tr > td {
|
||||
max-width: 150px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-thead > tr > th {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 16px !important;
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
min-width: calc(50% - 8px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stat-card {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.filter-form .ant-form-item {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<style>{generateGlobalStyles(designTokens)}</style>
|
||||
|
||||
<div style={headerStyle}>
|
||||
<div style={titleRowStyle}>
|
||||
@@ -1586,7 +1181,18 @@ function DeviceManagement() {
|
||||
批量删除 ({selectedDevices.length})
|
||||
</Button>
|
||||
<Button
|
||||
style={dangerActionStyle}
|
||||
style={{
|
||||
height: '36px',
|
||||
borderRadius: designTokens.borderRadius.small,
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
background: designTokens.colors.error.main,
|
||||
border: 'none',
|
||||
color: '#ffffff'
|
||||
}}
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDeleteAll}
|
||||
>
|
||||
@@ -1862,7 +1468,7 @@ function DeviceManagement() {
|
||||
<Form.Item style={{ textAlign: 'right', marginTop: '24px' }}>
|
||||
<Space>
|
||||
<Button onClick={handleCancel} style={secondaryActionStyle}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" style={primaryButtonStyle}>确定</Button>
|
||||
<Button type="primary" htmlType="submit" style={{ height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>确定</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -1910,7 +1516,7 @@ function DeviceManagement() {
|
||||
<Space>
|
||||
<Button onClick={() => setFieldConfigModalVisible(false)} style={secondaryActionStyle}>取消</Button>
|
||||
<Button onClick={handleResetFieldConfig} style={secondaryActionStyle}>重置默认</Button>
|
||||
<Button type="primary" htmlType="submit" style={primaryButtonStyle}>保存</Button>
|
||||
<Button type="primary" htmlType="submit" style={{ height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>保存</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -1995,7 +1601,7 @@ function DeviceManagement() {
|
||||
beforeUpload={handleImport}
|
||||
maxCount={1}
|
||||
>
|
||||
<Button type="primary" icon={<UploadOutlined />} block style={primaryButtonStyle}>
|
||||
<Button type="primary" icon={<UploadOutlined />} block style={{ height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
选择CSV文件
|
||||
</Button>
|
||||
</Upload>
|
||||
@@ -2086,7 +1692,7 @@ function DeviceManagement() {
|
||||
setImportResult(null);
|
||||
setIsImporting(false);
|
||||
fetchDevices();
|
||||
}} style={{ marginTop: '20px', ...primaryButtonStyle, height: '40px' }}>
|
||||
}} style={{ marginTop: '20px', height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2115,7 +1721,7 @@ function DeviceManagement() {
|
||||
<Button key="edit" type="primary" onClick={() => {
|
||||
setDetailModalVisible(false);
|
||||
showModal(selectedDevice);
|
||||
}} style={primaryButtonStyle}>
|
||||
}} style={{ height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
编辑
|
||||
</Button>
|
||||
]}
|
||||
@@ -2240,7 +1846,7 @@ function DeviceManagement() {
|
||||
<Button key="cancel" onClick={() => setBatchStatusModalVisible(false)} style={secondaryActionStyle}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" loading={batchStatusLoading} onClick={handleBatchStatusChange} style={primaryButtonStyle}>
|
||||
<Button key="submit" type="primary" loading={batchStatusLoading} onClick={handleBatchStatusChange} style={{ height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
确定
|
||||
</Button>
|
||||
]}
|
||||
@@ -2279,7 +1885,7 @@ function DeviceManagement() {
|
||||
<Button key="cancel" onClick={() => setExportModalVisible(false)} style={secondaryActionStyle}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" loading={exportLoading} onClick={handleEnhancedExport} style={primaryButtonStyle}>
|
||||
<Button key="submit" type="primary" loading={exportLoading} onClick={handleEnhancedExport} style={{ height: '40px', borderRadius: designTokens.borderRadius.small, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, fontWeight: '500', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
导出
|
||||
</Button>
|
||||
]}
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
/**
|
||||
* 设备管理页面样式配置
|
||||
* 集中管理所有内联样式对象
|
||||
*/
|
||||
|
||||
import { designTokens } from '../config/theme';
|
||||
|
||||
const { colors, shadows, borderRadius, transitions, spacing } = designTokens;
|
||||
|
||||
// 页面容器样式
|
||||
export const pageContainerStyle = {
|
||||
minHeight: '100vh',
|
||||
background: colors.background.secondary,
|
||||
padding: spacing.lg
|
||||
};
|
||||
|
||||
// 头部样式
|
||||
export const headerStyle = {
|
||||
marginBottom: spacing.lg
|
||||
};
|
||||
|
||||
// 标题行样式
|
||||
export const titleRowStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.lg,
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.md
|
||||
};
|
||||
|
||||
// 标题区域样式
|
||||
export const titleSectionStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md
|
||||
};
|
||||
|
||||
// 标题图标样式
|
||||
export const titleIconStyle = {
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
borderRadius: borderRadius.medium,
|
||||
background: colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: shadows.medium
|
||||
};
|
||||
|
||||
// 标题文本样式
|
||||
export const titleTextStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px'
|
||||
};
|
||||
|
||||
// 页面标题样式
|
||||
export const pageTitleStyle = {
|
||||
fontSize: '22px',
|
||||
fontWeight: '700',
|
||||
margin: 0,
|
||||
color: colors.text.primary,
|
||||
lineHeight: 1.2
|
||||
};
|
||||
|
||||
// 页面副标题样式
|
||||
export const pageSubtitleStyle = {
|
||||
fontSize: '13px',
|
||||
color: colors.text.secondary,
|
||||
margin: 0
|
||||
};
|
||||
|
||||
// 操作按钮基础样式
|
||||
export const actionButtonStyle = {
|
||||
height: '36px',
|
||||
borderRadius: borderRadius.small,
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
};
|
||||
|
||||
// 主要操作按钮样式
|
||||
export const primaryActionStyle = {
|
||||
...actionButtonStyle,
|
||||
background: colors.primary.gradient,
|
||||
border: 'none',
|
||||
color: '#ffffff !important',
|
||||
boxShadow: shadows.small
|
||||
};
|
||||
|
||||
// 次要操作按钮样式
|
||||
export const secondaryActionStyle = {
|
||||
...actionButtonStyle,
|
||||
background: colors.background.primary,
|
||||
border: `1px solid ${colors.border.light}`,
|
||||
color: colors.text.primary
|
||||
};
|
||||
|
||||
// 危险操作按钮样式
|
||||
export const dangerActionStyle = {
|
||||
...actionButtonStyle,
|
||||
background: colors.error.main,
|
||||
border: 'none',
|
||||
color: '#ffffff'
|
||||
};
|
||||
|
||||
// 主要按钮样式(大)
|
||||
export const primaryButtonStyle = {
|
||||
height: '40px',
|
||||
borderRadius: borderRadius.small,
|
||||
background: colors.primary.gradient,
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
boxShadow: shadows.small,
|
||||
fontWeight: '500',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
};
|
||||
|
||||
// 统计卡片行样式
|
||||
export const statsRowStyle = {
|
||||
display: 'flex',
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
flexWrap: 'wrap'
|
||||
};
|
||||
|
||||
// 统计卡片基础样式
|
||||
export const statCardStyle = {
|
||||
flex: 1,
|
||||
minWidth: '140px',
|
||||
maxWidth: '200px',
|
||||
padding: `${spacing.md} ${spacing.lg}`,
|
||||
background: colors.background.primary,
|
||||
borderRadius: borderRadius.medium,
|
||||
border: `1px solid ${colors.border.light}`,
|
||||
boxShadow: shadows.small,
|
||||
transition: `all ${transitions.fast}`
|
||||
};
|
||||
|
||||
// 统计数值样式
|
||||
export const statValueStyle = {
|
||||
fontSize: '24px',
|
||||
fontWeight: '700',
|
||||
color: colors.text.primary,
|
||||
lineHeight: 1.2
|
||||
};
|
||||
|
||||
// 统计标签样式
|
||||
export const statLabelStyle = {
|
||||
fontSize: '12px',
|
||||
color: colors.text.secondary,
|
||||
marginTop: '4px'
|
||||
};
|
||||
|
||||
// 运行中状态统计卡片样式
|
||||
export const statCardRunningStyle = {
|
||||
...statCardStyle,
|
||||
borderLeft: `3px solid ${colors.success.main}`,
|
||||
background: `${colors.success.main}08`
|
||||
};
|
||||
|
||||
// 维护中状态统计卡片样式
|
||||
export const statCardMaintenanceStyle = {
|
||||
...statCardStyle,
|
||||
borderLeft: `3px solid ${colors.warning.main}`,
|
||||
background: `${colors.warning.main}08`
|
||||
};
|
||||
|
||||
// 故障状态统计卡片样式
|
||||
export const statCardFaultStyle = {
|
||||
...statCardStyle,
|
||||
borderLeft: `3px solid ${colors.error.main}`,
|
||||
background: `${colors.error.main}08`
|
||||
};
|
||||
|
||||
// 卡片基础样式
|
||||
export const cardStyle = {
|
||||
borderRadius: borderRadius.large,
|
||||
border: 'none',
|
||||
boxShadow: shadows.medium,
|
||||
overflow: 'hidden',
|
||||
background: colors.background.primary
|
||||
};
|
||||
|
||||
// 筛选卡片样式
|
||||
export const filterCardStyle = {
|
||||
borderRadius: borderRadius.medium,
|
||||
border: 'none',
|
||||
boxShadow: shadows.small,
|
||||
background: colors.background.primary,
|
||||
marginBottom: spacing.lg
|
||||
};
|
||||
|
||||
// 模态框头部样式
|
||||
export const modalHeaderStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
fontSize: '18px',
|
||||
fontWeight: '600'
|
||||
};
|
||||
|
||||
// 表格样式常量
|
||||
export const tableStyles = {
|
||||
// 表格容器样式
|
||||
wrapper: {
|
||||
borderRadius: borderRadius.medium,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
// 空状态样式
|
||||
empty: {
|
||||
textAlign: 'center',
|
||||
padding: '60px 20px',
|
||||
color: colors.text.secondary,
|
||||
fontSize: '15px'
|
||||
},
|
||||
|
||||
// 空状态图标样式
|
||||
emptyIcon: {
|
||||
fontSize: '48px',
|
||||
marginBottom: '16px',
|
||||
color: colors.border.light
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索输入框样式
|
||||
export const searchInputStyle = {
|
||||
width: '280px',
|
||||
borderRadius: borderRadius.medium,
|
||||
border: `1px solid ${colors.border.light}`,
|
||||
transition: `all ${transitions.fast}`
|
||||
};
|
||||
|
||||
// 选择器样式
|
||||
export const selectStyle = {
|
||||
borderRadius: borderRadius.medium
|
||||
};
|
||||
|
||||
// 下拉菜单样式
|
||||
export const dropdownStyle = {
|
||||
borderRadius: borderRadius.medium
|
||||
};
|
||||
|
||||
// 刷新按钮样式
|
||||
export const refreshButtonStyle = {
|
||||
borderRadius: borderRadius.medium,
|
||||
border: `1px solid ${colors.border.light}`,
|
||||
height: '36px'
|
||||
};
|
||||
|
||||
// 搜索按钮样式
|
||||
export const searchButtonStyle = {
|
||||
height: '36px',
|
||||
borderRadius: borderRadius.medium,
|
||||
background: colors.primary.gradient,
|
||||
border: 'none',
|
||||
boxShadow: shadows.small
|
||||
};
|
||||
|
||||
// 重置按钮样式
|
||||
export const resetButtonStyle = {
|
||||
height: '36px',
|
||||
borderRadius: borderRadius.medium,
|
||||
border: `1px solid ${colors.border.light}`
|
||||
};
|
||||
|
||||
// 导入模态框样式
|
||||
export const importModalStyles = {
|
||||
// 说明区域样式
|
||||
description: {
|
||||
marginBottom: '20px',
|
||||
padding: '16px',
|
||||
background: 'linear-gradient(180deg, #fafafa 0%, #ffffff 100%)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #f0f0f0'
|
||||
},
|
||||
|
||||
// 标题样式
|
||||
title: {
|
||||
fontWeight: '600',
|
||||
marginBottom: '8px',
|
||||
color: '#333'
|
||||
},
|
||||
|
||||
// 列表样式
|
||||
list: {
|
||||
paddingLeft: '20px',
|
||||
marginBottom: '10px',
|
||||
color: '#666',
|
||||
fontSize: '13px',
|
||||
marginTop: '12px'
|
||||
},
|
||||
|
||||
// 进度容器样式
|
||||
progressContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
|
||||
// 进度图标样式
|
||||
progressIcon: {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%',
|
||||
background: colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: '16px',
|
||||
color: '#fff',
|
||||
fontSize: '20px'
|
||||
},
|
||||
|
||||
// 进度信息样式
|
||||
progressInfo: {
|
||||
title: {
|
||||
margin: '0 0 4px 0',
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
fontSize: '16px'
|
||||
},
|
||||
phase: {
|
||||
margin: 0,
|
||||
color: colors.primary.main,
|
||||
fontSize: '14px'
|
||||
}
|
||||
},
|
||||
|
||||
// 结果卡片样式
|
||||
resultCard: (type) => ({
|
||||
padding: '12px',
|
||||
background: type === 'total' ? colors.primary.gradient :
|
||||
type === 'success' ? 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)' :
|
||||
'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)',
|
||||
borderRadius: '8px',
|
||||
color: '#fff',
|
||||
textAlign: 'center'
|
||||
}),
|
||||
|
||||
// 结果数值样式
|
||||
resultValue: {
|
||||
fontSize: '24px',
|
||||
fontWeight: '700'
|
||||
},
|
||||
|
||||
// 结果标签样式
|
||||
resultLabel: {
|
||||
fontSize: '12px',
|
||||
opacity: 0.9
|
||||
}
|
||||
};
|
||||
|
||||
// 详情模态框样式
|
||||
export const detailModalStyles = {
|
||||
// 信息项样式
|
||||
infoItem: {
|
||||
label: {
|
||||
fontWeight: '500',
|
||||
color: '#666'
|
||||
},
|
||||
value: {
|
||||
marginLeft: 8,
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
|
||||
// 描述区域样式
|
||||
description: {
|
||||
marginTop: '16px'
|
||||
},
|
||||
|
||||
// 描述内容样式
|
||||
descriptionContent: {
|
||||
marginTop: '8px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '8px',
|
||||
color: '#333'
|
||||
}
|
||||
};
|
||||
|
||||
// 导出模态框样式
|
||||
export const exportModalStyles = {
|
||||
// 字段选择区域样式
|
||||
fieldSelector: {
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: '8px',
|
||||
padding: '12px'
|
||||
},
|
||||
|
||||
// 字段项样式
|
||||
fieldItem: {
|
||||
marginBottom: '8px'
|
||||
}
|
||||
};
|
||||
|
||||
// CSS-in-JS 样式字符串生成函数
|
||||
export const generateGlobalStyles = (tokens) => `
|
||||
.device-modal .ant-modal-close {
|
||||
top: 16px;
|
||||
right: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
alignItems: center;
|
||||
justifyContent: center;
|
||||
}
|
||||
|
||||
.device-modal .ant-modal-close-x {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
alignItems: center;
|
||||
justifyContent: center;
|
||||
}
|
||||
|
||||
.device-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-container {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-content {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-thead {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-thead > tr > th {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 14px 12px !important;
|
||||
background: ${tokens.colors.background.tertiary} !important;
|
||||
color: ${tokens.colors.text.primary} !important;
|
||||
border-bottom: 1px solid ${tokens.colors.border.light} !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody {
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr > td {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
line-height: 1.6 !important;
|
||||
max-width: 250px !important;
|
||||
padding: 12px !important;
|
||||
border-bottom: 1px solid ${tokens.colors.border.light} !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr > td .ant-typography,
|
||||
.device-table-wrapper .ant-table-tbody > tr > td .ant-typography-expand,
|
||||
.device-table-wrapper .ant-table-tbody > tr > td span {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-cell {
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-even {
|
||||
background-color: ${tokens.colors.background.primary};
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-odd {
|
||||
background-color: ${tokens.colors.background.secondary};
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-selected {
|
||||
background-color: ${tokens.colors.primary.main}15 !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-row-selected:hover > td {
|
||||
background-color: ${tokens.colors.primary.main}25 !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr:hover > td {
|
||||
background-color: ${tokens.colors.background.tertiary} !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-selection-column {
|
||||
position: sticky !important;
|
||||
left: 0 !important;
|
||||
z-index: 2 !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-tbody > tr > td:last-child {
|
||||
min-width: 120px !important;
|
||||
max-width: 150px !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-pagination {
|
||||
margin: 16px 0 !important;
|
||||
flex-wrap: wrap !important;
|
||||
justify-content: center !important;
|
||||
padding: 12px 16px !important;
|
||||
background: ${tokens.colors.background.primary};
|
||||
border-radius: ${tokens.borderRadius.medium};
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-pagination-item-active {
|
||||
background: ${tokens.colors.primary.gradient} !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-pagination-item-active a {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-cell-fix-left,
|
||||
.device-table-wrapper .ant-table-cell-fix-right {
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.device-table-wrapper .ant-table-tbody > tr > td {
|
||||
max-width: 150px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.device-table-wrapper .ant-table-thead > tr > th {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 16px !important;
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
min-width: calc(50% - 8px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stat-card {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.filter-form .ant-form-item {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user