diff --git a/backend/initDeviceFields.js b/backend/initDeviceFields.js index bd287c6..3aed9a2 100644 --- a/backend/initDeviceFields.js +++ b/backend/initDeviceFields.js @@ -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}`); } } diff --git a/backend/routes/devices.js b/backend/routes/devices.js index eaad8af..e35e9ce 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -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 }); } }); diff --git a/frontend/src/config/theme.js b/frontend/src/config/theme.js new file mode 100644 index 0000000..f568419 --- /dev/null +++ b/frontend/src/config/theme.js @@ -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; diff --git a/frontend/src/constants/deviceManagementConstants.js b/frontend/src/constants/deviceManagementConstants.js new file mode 100644 index 0000000..63c8137 --- /dev/null +++ b/frontend/src/constants/deviceManagementConstants.js @@ -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}" 吗?此操作不可恢复。` +}; diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 13161b4..c8b07c8 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -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 (
- +
@@ -1586,7 +1181,18 @@ function DeviceManagement() { 批量删除 ({selectedDevices.length}) - + @@ -1910,7 +1516,7 @@ function DeviceManagement() { - + @@ -1995,7 +1601,7 @@ function DeviceManagement() { beforeUpload={handleImport} maxCount={1} > - @@ -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' }}> 确定
@@ -2115,7 +1721,7 @@ function DeviceManagement() { ]} @@ -2240,7 +1846,7 @@ function DeviceManagement() { , - ]} @@ -2279,7 +1885,7 @@ function DeviceManagement() { , - ]} diff --git a/frontend/src/styles/deviceManagementStyles.js b/frontend/src/styles/deviceManagementStyles.js new file mode 100644 index 0000000..7f1bd5d --- /dev/null +++ b/frontend/src/styles/deviceManagementStyles.js @@ -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; + } + } +`;