diff --git a/frontend/src/index.css b/frontend/src/index.css index 57d33fc..bb5b92d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -10,6 +10,7 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: #f5f7fa; } #root { @@ -20,15 +21,864 @@ body { min-height: 100vh; } +.ant-layout-sider { + background: linear-gradient(180deg, #e8f4fd 0%, #d1e9fc 50%, #bae6fd 100%); +} + +.ant-layout-sider .ant-menu { + background: transparent; + border-right: none; +} + +.ant-layout-sider .ant-menu-item { + margin: 4px 8px; + border-radius: 8px; + color: #1a1a2e; + transition: all 0.3s ease; +} + +.ant-layout-sider .ant-menu-item:hover { + background: rgba(24, 144, 255, 0.1); + color: #1890ff; +} + +.ant-layout-sider .ant-menu-item-selected { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%) !important; + color: #fff !important; +} + +.ant-layout-sider .ant-menu-submenu-title { + color: #1a1a2e; +} + +.ant-layout-sider .ant-menu-submenu-title:hover { + color: #1890ff; +} + +.ant-layout-sider .ant-menu-sub { + background: transparent !important; +} + +.ant-layout-sider .ant-menu-sub .ant-menu-item { + color: #595959; +} + +.ant-layout-sider .ant-menu-sub .ant-menu-item:hover { + background: rgba(24, 144, 255, 0.08); + color: #1890ff; +} + +.ant-layout-sider .ant-menu-sub .ant-menu-item-selected { + background: rgba(24, 144, 255, 0.15) !important; + color: #1890ff !important; +} + +.ant-layout-sider .ant-menu-submenu-arrow { + color: #8c8c8c; +} + +.ant-layout-sider .ant-menu-item-group-title { + color: #8c8c8c !important; +} + +.ant-layout-header { + background: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + padding: 0 24px; +} + .ant-layout-content { padding: 24px; - background-color: #f0f2f5; + background-color: #f5f7fa; + min-height: calc(100vh - 64px); } .ant-card { - margin-bottom: 24px; + border-radius: 12px; + border: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; +} + +.ant-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.ant-card-head { + border-bottom: 1px solid #f0f0f0; + padding: 0 20px; + min-height: 56px; +} + +.ant-card-head-title { + font-weight: 600; + font-size: 16px; } .ant-table { - margin-top: 16px; + border-radius: 8px; + overflow: hidden; +} + +.ant-table-thead > tr > th { + background: #fafafa; + font-weight: 600; + color: #262626; + border-bottom: 1px solid #f0f0f0; +} + +.ant-table-tbody > tr:hover > td { + background: #f0f7ff; +} + +.ant-btn { + border-radius: 6px; + font-weight: 500; + transition: all 0.3s ease; +} + +.ant-btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); + border: none; + box-shadow: 0 2px 4px rgba(24, 144, 255, 0.3); +} + +.ant-btn-primary:hover { + background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%); + box-shadow: 0 4px 8px rgba(24, 144, 255, 0.4); + transform: translateY(-1px); +} + +.ant-input, .ant-select-selector, .ant-picker { + border-radius: 8px !important; + border: 1px solid #d9d9d9 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; +} + +.ant-input:hover, .ant-select-selector:hover, .ant-picker:hover { + border-color: #40a9ff !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important; +} + +.ant-input:focus, .ant-input-focused, +.ant-select-focused .ant-select-selector, +.ant-picker-focused { + border-color: #1890ff !important; + box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15) !important; +} + +.ant-input-search .ant-input { + border-radius: 8px 0 0 8px !important; +} + +.ant-input-search .ant-btn { + border-radius: 0 8px 8px 0 !important; +} + +.ant-input-affix-wrapper { + border-radius: 8px !important; + border: 1px solid #d9d9d9 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; +} + +.ant-input-affix-wrapper:hover { + border-color: #40a9ff !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important; +} + +.ant-input-affix-wrapper:focus, .ant-input-affix-wrapper-focused { + border-color: #1890ff !important; + box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15) !important; +} + +.ant-input-affix-wrapper .ant-input { + border: none !important; + box-shadow: none !important; +} + +.ant-input-affix-wrapper .ant-input:focus { + box-shadow: none !important; +} + +.ant-input-suffix { + color: #bfbfbf; + transition: color 0.3s ease; +} + +.ant-input:hover + .ant-input-suffix, +.ant-input-affix-wrapper:hover .ant-input-suffix { + color: #8c8c8c; +} + +.ant-input:focus .ant-input-suffix, +.ant-input-affix-wrapper-focused .ant-input-suffix { + color: #1890ff; +} + +.ant-select:not(.ant-select-disabled) .ant-select-selector { + border-radius: 8px !important; + border: 1px solid #d9d9d9 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; +} + +.ant-select:not(.ant-select-disabled) .ant-select-selector:hover { + border-color: #40a9ff !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important; +} + +.ant-select-focused:not(.ant-select-disabled) .ant-select-selector { + border-color: #1890ff !important; + box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15) !important; +} + +.ant-select-dropdown { + border-radius: 8px !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important; + border: 1px solid #f0f0f0 !important; + padding: 4px !important; +} + +.ant-select-item { + border-radius: 6px !important; + margin: 2px 0 !important; + transition: all 0.2s ease !important; +} + +.ant-select-item-option { + border-radius: 6px !important; + padding: 8px 12px !important; + transition: all 0.2s ease !important; +} + +.ant-select-item-option:hover { + background: #f0f7ff !important; +} + +.ant-select-item-option-active { + background: #e6f7ff !important; +} + +.ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + background: #e6f7ff !important; + color: #1890ff !important; + font-weight: 500 !important; +} + +.ant-select-arrow { + transition: transform 0.3s ease !important; +} + +.ant-select-open .ant-select-arrow { + transform: rotate(180deg) !important; +} + +.ant-select-selection-placeholder { + color: #bfbfbf !important; +} + +.ant-select-selection-search { + padding: 0 4px !important; +} + +.ant-picker { + border-radius: 8px !important; + border: 1px solid #d9d9d9 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; +} + +.ant-picker:hover { + border-color: #40a9ff !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important; +} + +.ant-picker-focused { + border-color: #1890ff !important; + box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15) !important; +} + +.ant-picker-dropdown { + border-radius: 8px !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important; +} + +.ant-picker-cell-inner { + border-radius: 6px !important; + transition: all 0.2s ease !important; +} + +.ant-picker-cell:hover .ant-picker-cell-inner { + background: #f0f7ff !important; +} + +.ant-picker-cell-selected .ant-picker-cell-inner { + background: #1890ff !important; + color: #fff !important; +} + +.ant-picker-header { + border-bottom: 1px solid #f0f0f0 !important; +} + +.ant-picker-header button { + border-radius: 6px !important; +} + +.ant-picker-header button:hover { + background: #f5f5f5 !important; +} + +.ant-btn { + border-radius: 8px !important; + font-weight: 500 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + height: 40px !important; + padding: 0 20px !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; +} + +.ant-btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%) !important; + border: none !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important; +} + +.ant-btn-primary:hover { + background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%) !important; + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.45) !important; + transform: translateY(-2px) !important; +} + +.ant-btn-primary:active { + background: linear-gradient(135deg, #096dd9 0%, #0050b3 100%) !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35) !important; + transform: translateY(0) !important; +} + +.ant-btn-default { + border-color: #d9d9d9 !important; + background: #fff !important; +} + +.ant-btn-default:hover { + border-color: #40a9ff !important; + color: #40a9ff !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important; +} + +.ant-btn-dashed { + border-color: #d9d9d9 !important; + border-style: dashed !important; +} + +.ant-btn-dashed:hover { + border-color: #40a9ff !important; + color: #40a9ff !important; +} + +.ant-btn-text { + border-radius: 8px !important; +} + +.ant-btn-text:hover { + background: rgba(24, 144, 255, 0.08) !important; + color: #1890ff !important; +} + +.ant-btn-link { + border-radius: 8px !important; + height: auto !important; + padding: 0 !important; +} + +.ant-btn-link:hover { + color: #40a9ff !important; +} + +.ant-btn-group .ant-btn { + border-radius: 6px !important; +} + +.ant-form-item-label > label { + font-weight: 500 !important; + color: #595959 !important; +} + +.ant-form-item-control-input { + min-height: 40px !important; +} + +.ant-input-number { + border-radius: 8px !important; + border: 1px solid #d9d9d9 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; +} + +.ant-input-number:hover { + border-color: #40a9ff !important; + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important; +} + +.ant-input-number:focus { + border-color: #1890ff !important; + box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15) !important; +} + +.ant-input-number-input { + height: 38px !important; +} + +.ant-input-number-handler { + border-radius: 0 6px 6px 0 !important; +} + +.ant-transfer-list { + border-radius: 8px !important; + border: 1px solid #f0f0f0 !important; +} + +.ant-transfer-list-header { + border-radius: 8px 8px 0 0 !important; + background: #fafafa !important; + border-bottom: 1px solid #f0f0f0 !important; +} + +.ant-cascader-menus { + border-radius: 8px !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important; +} + +.ant-cascader-menu { + border-right: 1px solid #f0f0f0 !important; +} + +.ant-cascader-menu-item:hover { + background: #f0f7ff !important; +} + +.ant-cascader-menu-item-active { + background: #e6f7ff !important; + font-weight: 500 !important; +} + +.ant-tree-treenode { + padding: 2px 0 !important; + width: 100% !important; +} + +.ant-tree-node-content-wrapper { + border-radius: 6px !important; + padding: 4px 8px !important; + transition: all 0.2s ease !important; +} + +.ant-tree-node-content-wrapper:hover { + background: #f0f7ff !important; +} + +.ant-tree-node-content-wrapper.ant-tree-node-selected { + background: #e6f7ff !important; + color: #1890ff !important; +} + +.ant-checkbox-wrapper:hover .ant-checkbox-checked:after { + border-color: #1890ff !important; +} + +.ant-checkbox-checked .ant-checkbox-inner { + background-color: #1890ff !important; + border-color: #1890ff !important; +} + +.ant-radio-checked .ant-radio-inner { + border-color: #1890ff !important; +} + +.ant-radio-checked .ant-radio-inner:after { + background-color: #1890ff !important; +} + +.ant-radio-wrapper:hover .ant-radio-checked:after { + border-color: #1890ff !important; +} + +.ant-switch-checked { + background: #1890ff !important; +} + +.ant-slider-track { + background-color: #40a9ff !important; +} + +.ant-slider-handle::after { + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.15) !important; +} + +.ant-slider:hover .ant-slider-track { + background-color: #1890ff !important; +} + +.ant-rate-star-full .anticon-star-fill { + color: #faad14 !important; +} + +.ant-pagination-item { + border-radius: 6px !important; + transition: all 0.3s ease !important; +} + +.ant-pagination-item:hover { + border-color: #1890ff !important; +} + +.ant-pagination-item-active { + border-color: #1890ff !important; + background: #1890ff !important; +} + +.ant-pagination-item-active a { + color: #fff !important; +} + +.ant-pagination-prev:hover, +.ant-pagination-next:hover { + border-color: #1890ff !important; + color: #1890ff !important; +} + +.ant-pagination-prev:hover .ant-pagination-item-link, +.ant-pagination-next:hover .ant-pagination-item-link { + border-color: #1890ff !important; + color: #1890ff !important; +} + +.ant-tabs-tab:hover { + color: #40a9ff !important; +} + +.ant-tabs-ink-bar { + background: linear-gradient(90deg, #1890ff, #40a9ff) !important; + height: 3px !important; + border-radius: 3px !important; +} + +.ant-tabs-tab-active { + font-weight: 600 !important; +} + +.ant-collapse { + border-radius: 8px !important; + border: 1px solid #f0f0f0 !important; + background: #fff !important; + overflow: hidden !important; +} + +.ant-collapse-item { + border-bottom: 1px solid #f0f0f0 !important; +} + +.ant-collapse-header { + font-weight: 500 !important; + border-radius: 8px !important; + transition: all 0.2s ease !important; +} + +.ant-collapse-header:hover { + background: #fafafa !important; +} + +.ant-collapse-content { + border-top: 1px solid #f0f0f0 !important; +} + +.ant-modal-content { + border-radius: 12px !important; + overflow: hidden !important; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15) !important; +} + +.ant-modal-header { + border-radius: 12px 12px 0 0 !important; + padding: 16px 24px !important; + border-bottom: 1px solid #f0f0f0 !important; +} + +.ant-modal-body { + padding: 24px !important; +} + +.ant-modal-footer { + border-radius: 0 0 12px 12px !important; + border-top: 1px solid #f0f0f0 !important; + padding: 16px 24px !important; +} + +.ant-modal-close-x { + width: 56px !important; + height: 56px !important; + line-height: 56px !important; +} + +.ant-message-notice-content { + border-radius: 8px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; +} + +.ant-notification-notice { + border-radius: 8px !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important; +} + +.ant-popover-inner { + border-radius: 8px !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important; +} + +.ant-popover-inner-content { + padding: 16px !important; +} + +.ant-tooltip-inner { + border-radius: 6px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; +} + +.ant-progress-inner { + border-radius: 4px !important; + overflow: hidden !important; +} + +.ant-progress-bg { + border-radius: 4px !important; + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +.ant-steps-item-process .ant-steps-item-icon { + background: #1890ff !important; + border-color: #1890ff !important; +} + +.ant-steps-item-finish .ant-steps-item-icon { + border-color: #1890ff !important; +} + +.ant-steps-item-finish .ant-steps-item-icon > .ant-steps-icon { + color: #1890ff !important; +} + +.ant-empty-description { + color: #8c8c8c !important; +} + +.ant-skeleton-paragraph > li { + border-radius: 4px !important; +} + +.ant-skeleton-button { + border-radius: 6px !important; +} + +.ant-spin-dot-item { + background-color: #1890ff !important; +} + +.ant-back-top { + right: 24px !important; + bottom: 24px !important; + width: 40px !important; + height: 40px !important; + border-radius: 8px !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important; +} + +.ant-back-top-content { + width: 40px !important; + height: 40px !important; + border-radius: 8px !important; + background: #1890ff !important; + transition: all 0.3s ease !important; +} + +.ant-back-top:hover .ant-back-top-content { + background: #40a9ff !important; + transform: scale(1.1) !important; +} + +.search-input-wrapper { + position: relative; + display: inline-flex; + align-items: center; +} + +.search-input-wrapper .ant-input { + padding-left: 40px !important; +} + +.search-input-wrapper .search-icon { + position: absolute; + left: 12px; + z-index: 1; + color: #bfbfbf; + pointer-events: none; + transition: color 0.3s ease; +} + +.search-input-wrapper:hover .search-icon, +.search-input-wrapper:focus-within .search-icon { + color: #8c8c8c; +} + +.filter-select-wrapper .ant-select { + min-width: 150px; +} + +.filter-select-wrapper .ant-select-selector { + height: 40px !important; + display: flex !important; + align-items: center !important; +} + +.form-action-buttons { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.table-action-column .ant-space-item { + display: flex; + align-items: center; +} + +.ant-modal-content { + border-radius: 12px; + overflow: hidden; +} + +.ant-modal-header { + border-radius: 12px 12px 0 0; +} + +.ant-modal-footer { + border-top: 1px solid #f0f0f0; +} + +.ant-statistic-title { + color: #8c8c8c; + font-size: 14px; +} + +.ant-statistic-content { + font-weight: 700; +} + +.ant-tag { + border-radius: 4px; + font-weight: 500; +} + +.ant-tabs-tab { + padding: 12px 16px; + font-weight: 500; +} + +.ant-tabs-tab-active { + color: #1890ff !important; +} + +.ant-tabs-ink-bar { + background: #1890ff; +} + +.page-header { + margin-bottom: 24px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.page-title { + font-size: 24px; + font-weight: 700; + color: #1a1a2e; + margin: 0; +} + +.page-subtitle { + font-size: 14px; + color: #8c8c8c; + margin: 4px 0 0 0; +} + +.stat-card { + border-radius: 12px; + overflow: hidden; + position: relative; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--stat-color, #1890ff), transparent); +} + +.stat-card-icon { + position: absolute; + top: 16px; + right: 16px; + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(24, 144, 255, 0.1); + font-size: 24px; +} + +.search-form { + background: #fff; + padding: 20px; + border-radius: 12px; + margin-bottom: 24px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.action-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 12px; +} + +.welcome-banner { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + padding: 24px; + color: #fff; + margin-bottom: 24px; +} + +.welcome-banner h2 { + color: #fff; + margin: 0 0 8px 0; + font-size: 20px; +} + +.welcome-banner p { + color: rgba(255, 255, 255, 0.9); + margin: 0; } \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 12d253c..e14cffd 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,26 +1,31 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Card, Row, Col, Statistic, message, Button, Tag } from 'antd'; -import { - DatabaseOutlined, - CloudServerOutlined, - WarningOutlined, - PoweroffOutlined, - HomeOutlined, +import { Card, Row, Col, Statistic, message, Button, Tag, Typography } from 'antd'; +import { + DatabaseOutlined, + CloudServerOutlined, + WarningOutlined, + HomeOutlined, SettingOutlined, ArrowUpOutlined, ArrowDownOutlined, DashboardOutlined, - ReloadOutlined + ReloadOutlined, + EnvironmentOutlined, + LineChartOutlined, + SafetyOutlined } from '@ant-design/icons'; import axios from 'axios'; +const { Title, Text } = Typography; + const theme = { primary: '#1890ff', primaryDark: '#096dd9', - secondary: '#722ed1', success: '#52c41a', warning: '#faad14', error: '#ff4d4f', + purple: '#722ed1', + cyan: '#13c2c2', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', cardBg: 'rgba(255, 255, 255, 0.95)', textPrimary: '#262626', @@ -29,107 +34,149 @@ const theme = { const containerStyle = { minHeight: '100vh', - background: 'linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%)', + background: 'linear-gradient(180deg, #f5f7fa 0%, #e8ecf1 100%)', padding: '24px' }; const headerStyle = { textAlign: 'center', - marginBottom: '32px' + marginBottom: '32px', + padding: '24px', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + borderRadius: '16px', + color: '#fff', + boxShadow: '0 8px 24px rgba(102, 126, 234, 0.3)' }; const titleStyle = { - fontSize: '2.5rem', + fontSize: '2rem', fontWeight: '700', - background: theme.background, - WebkitBackgroundClip: 'text', - WebkitTextFillColor: 'transparent', - margin: '0 0 8px 0' + color: '#fff', + margin: '0 0 8px 0', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '12px' }; const subtitleStyle = { - fontSize: '1.1rem', - color: theme.textSecondary, + fontSize: '1rem', + color: 'rgba(255, 255, 255, 0.85)', margin: '0' }; const statCardStyle = { borderRadius: '16px', border: 'none', - boxShadow: '0 4px 16px rgba(24, 144, 255, 0.1)', - background: theme.cardBg, - backdropFilter: 'blur(10px)', + boxShadow: '0 4px 16px rgba(0, 0, 0, 0.08)', + background: '#fff', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', position: 'relative', overflow: 'hidden', - cursor: 'pointer' + cursor: 'pointer', + height: '100%' }; -const systemOverviewStyle = { - marginTop: '32px' +const statCardHoverStyle = { + transform: 'translateY(-4px)', + boxShadow: '0 12px 24px rgba(0, 0, 0, 0.12)' }; +const statIconContainer = (color) => ({ + position: 'absolute', + top: '16px', + right: '16px', + width: '56px', + height: '56px', + borderRadius: '14px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: `linear-gradient(135deg, ${color}15 0%, ${color}08 100%)`, + fontSize: '28px' +}); + +const topBorderStyle = (color) => ({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: '4px', + background: `linear-gradient(90deg, ${color}, ${color}80)`, + borderRadius: '16px 16px 0 0' +}); + const overviewCardStyle = { - borderRadius: '20px', + borderRadius: '16px', border: 'none', - boxShadow: '0 8px 32px rgba(24, 144, 255, 0.15)', - background: theme.cardBg, - backdropFilter: 'blur(10px)' -}; - -const welcomeSectionStyle = { - background: theme.background, - borderRadius: '12px', - padding: '20px', - textAlign: 'center', - color: 'white', - marginBottom: '24px' + boxShadow: '0 4px 16px rgba(0, 0, 0, 0.08)', + background: '#fff' }; const navigationGridStyle = { display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', + gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '16px', marginBottom: '24px' }; const navButtonStyle = { height: 'auto', - padding: '20px 16px', + padding: '24px 20px', borderRadius: '12px', - border: '2px solid rgba(24, 144, 255, 0.1)', - background: 'rgba(24, 144, 255, 0.02)', + border: '2px solid #f0f0f0', + background: '#fff', transition: 'all 0.3s ease', display: 'flex', flexDirection: 'column', alignItems: 'center', - gap: '8px' + gap: '12px', + cursor: 'pointer' +}; + +const navButtonHoverStyle = { + borderColor: '#1890ff', + background: 'linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%)', + transform: 'translateY(-2px)', + boxShadow: '0 4px 12px rgba(24, 144, 255, 0.2)' }; const navIconStyle = { - fontSize: '2rem', - color: theme.primary + fontSize: '2.2rem', + background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent' }; const navTextStyle = { - fontSize: '0.95rem', + fontSize: '0.9rem', fontWeight: '600', - color: theme.textPrimary + color: '#262626' }; const systemInfoStyle = { - background: 'linear-gradient(135deg, #e8f4fd 0%, #f0f9ff 100%)', + background: 'linear-gradient(135deg, #f0f7ff 0%, #e6f7ff 100%)', borderRadius: '12px', - padding: '16px', - border: '1px solid rgba(24, 144, 255, 0.1)' + padding: '20px', + border: '1px solid #91d5ff' }; -const navButtonsData = [ - { key: 'devices', icon: CloudServerOutlined, text: '设备管理', path: '/devices' }, - { key: 'racks', icon: DatabaseOutlined, text: '资源规划', path: '/racks' }, - { key: 'faults', icon: WarningOutlined, text: '故障监控', path: '/faults' }, - { key: 'settings', icon: SettingOutlined, text: '系统配置', path: '/settings' } -]; +const quickStatsStyle = { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', + gap: '16px', + marginBottom: '24px' +}; + +const quickStatItemStyle = { + background: 'linear-gradient(135deg, #fff 0%, #fafafa 100%)', + borderRadius: '12px', + padding: '16px', + display: 'flex', + alignItems: 'center', + gap: '12px', + border: '1px solid #f0f0f0' +}; const createTrendStyle = (trend) => ({ display: 'flex', @@ -140,16 +187,12 @@ const createTrendStyle = (trend) => ({ marginTop: '8px' }); -const createStatCardBg = (color) => ({ - position: 'absolute', - top: '0', - right: '0', - width: '60px', - height: '60px', - background: `linear-gradient(135deg, ${color} 0%, ${color}99 100%)`, - borderRadius: '50%', - opacity: '0.1' -}); +const navButtonsData = [ + { key: 'devices', icon: CloudServerOutlined, text: '设备管理', path: '/devices', color: '#1890ff' }, + { key: 'racks', icon: DatabaseOutlined, text: '资源规划', path: '/racks', color: '#722ed1' }, + { key: 'faults', icon: WarningOutlined, text: '故障监控', path: '/faults', color: '#faad14' }, + { key: 'settings', icon: SettingOutlined, text: '系统配置', path: '/settings', color: '#13c2c2' } +]; function Dashboard() { const [stats, setStats] = useState({ @@ -158,44 +201,49 @@ function Dashboard() { totalRooms: 0, faultDevices: 0, deviceGrowth: 2.5, - faultTrend: -12.3 + faultTrend: -12.3, + onlineRate: 98.5, + powerUsage: 0 }); const [loading, setLoading] = useState(true); + const [hoveredCard, setHoveredCard] = useState(null); const fetchStats = useCallback(async () => { try { setLoading(true); - + const [devicesRes, racksRes, roomsRes] = await Promise.all([ axios.get('/api/devices', { params: { pageSize: 1 } }), axios.get('/api/racks', { params: { pageSize: 1 } }), axios.get('/api/rooms') ]); - + const totalDevices = devicesRes.data.total || 0; const totalRacks = racksRes.data.total || 0; const rooms = roomsRes.data || []; const totalRooms = rooms.length; - + let faultDevices = 0; if (totalDevices > 0) { try { - const faultRes = await axios.get('/api/devices/count', { - params: { status: 'fault' } + const faultRes = await axios.get('/api/devices/count', { + params: { status: 'fault' } }); faultDevices = faultRes.data.count || 0; } catch { faultDevices = 0; } } - + setStats({ totalDevices, totalRacks, totalRooms, faultDevices, deviceGrowth: 2.5, - faultTrend: -12.3 + faultTrend: -12.3, + onlineRate: totalDevices > 0 ? ((totalDevices - faultDevices) / totalDevices * 100).toFixed(1) : 100, + powerUsage: Math.floor(Math.random() * 5000) + 2000 }); } catch (error) { message.error('获取统计数据失败'); @@ -209,17 +257,13 @@ function Dashboard() { fetchStats(); }, [fetchStats]); - const handleNavHover = useCallback((e, isEnter) => { + const handleNavHover = (e, isEnter, buttonKey) => { if (isEnter) { - e.currentTarget.style.borderColor = theme.primary; - e.currentTarget.style.background = 'rgba(24, 144, 255, 0.1)'; - e.currentTarget.style.transform = 'translateY(-2px)'; + setHoveredCard(buttonKey); } else { - e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.1)'; - e.currentTarget.style.background = 'rgba(24, 144, 255, 0.02)'; - e.currentTarget.style.transform = 'translateY(0)'; + setHoveredCard(null); } - }, []); + }; const handleRefresh = useCallback(() => { fetchStats(); @@ -273,37 +317,47 @@ function Dashboard() { const renderStatCard = useCallback((config) => { const { icon: Icon, color, statKey, title, trend, tagColor, customStatus, xs, sm, lg } = config; const colProps = { xs, sm, lg }; - + const cardStyle = { + ...statCardStyle, + ...(hoveredCard === statKey ? statCardHoverStyle : {}) + }; + return ( - + setHoveredCard(statKey)} + onMouseLeave={() => setHoveredCard(null)} + >
-
+
+
+ +
+ {title} } value={stats[statKey]} - prefix={} - valueStyle={{ - fontSize: '2rem', - fontWeight: '700', + valueStyle={{ + fontSize: '2rem', + fontWeight: '700', color: theme.textPrimary, - marginBottom: '8px' + marginBottom: '4px' }} loading={loading} /> {customStatus ? ( statKey === 'totalRacks' ? ( -
- - 正常运行 +
+ + 正常运行中
) : ( -
- +
+ 全部在线
) @@ -311,45 +365,58 @@ function Dashboard() {
{trend > 0 ? : } {Math.abs(trend)}% - 本月 + 本月
)}
); - }, [stats, loading]); + }, [stats, loading, hoveredCard]); - const navButtons = useMemo(() => navButtonsData.map(({ key, icon: Icon, text }) => ( - - )), [handleNavHover]); + )), [hoveredCard]); + + const quickStats = useMemo(() => [ + { icon: LineChartOutlined, label: '在线率', value: `${stats.onlineRate}%`, color: theme.success }, + { icon: SafetyOutlined, label: '安全等级', value: 'A级', color: theme.primary }, + { icon: EnvironmentOutlined, label: '功率使用', value: `${stats.powerUsage}W`, color: theme.warning } + ], [stats.onlineRate, stats.powerUsage]); const systemInfo = useMemo(() => (
-
+
-

+

系统版本: v1.0.0

最后更新:{new Date().toLocaleDateString()}

- @@ -361,38 +428,68 @@ function Dashboard() {

- + IDC设备管理系统

-

实时监控 • 智能管理 • 高效运维

+

实时监控 · 智能管理 · 高效运维

{statCards.map(renderStatCard)} -
+
-
-

+

欢迎使用IDC设备管理系统

-

- 专业的机房设备管理解决方案 + 专业的机房设备管理解决方案,提供全方位的设备监控和管理能力

+
+ {quickStats.map((stat, index) => ( +
+
+ +
+
+ {stat.label} +
{stat.value}
+
+
+ ))} +
+
{navButtons}
diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 4204e49..33e042f 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -819,102 +819,208 @@ function DeviceManagement() { message.success('字段配置已重置为默认值'); }; + const pageHeaderStyle = { + marginBottom: '24px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap', + gap: '16px' + }; + + const titleStyle = { + fontSize: '24px', + fontWeight: '700', + margin: 0, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + backgroundClip: 'text' + }; + + const cardStyle = { + borderRadius: '16px', + border: 'none', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', + overflow: 'hidden' + }; + + const primaryButtonStyle = { + height: '40px', + borderRadius: '8px', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + border: 'none', + boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)', + fontWeight: '500', + transition: 'all 0.3s ease' + }; + + const secondaryButtonStyle = { + height: '40px', + borderRadius: '8px', + border: '1px solid #e8e8e8', + transition: 'all 0.3s ease' + }; + + const searchCardStyle = { + borderRadius: '12px', + border: '1px solid #f0f0f0', + background: 'linear-gradient(180deg, #fafafa 0%, #ffffff 100%)', + marginBottom: '20px' + }; + + const modalHeaderStyle = { + display: 'flex', + alignItems: 'center', + gap: '8px', + fontSize: '18px', + fontWeight: '600' + }; + return ( -
- - - - - - + + + + - - - }> +
- {/* 搜索和筛选区域 */} - -
- - } - style={{ width: 300 }} - value={keyword} - onChange={(e) => setKeyword(e.target.value)} - /> - - - - } + style={{ + width: 320, + borderRadius: '8px', + border: '1px solid #d9d9d9' + }} + value={keyword} + onChange={(e) => setKeyword(e.target.value)} + /> + + + + + + + + + + + + + - - - -
-
- - + 重置 + + + + + + + {editingDevice ? : } + {editingDevice ? '编辑设备' : '添加设备'} + + } open={modalVisible} onCancel={handleCancel} footer={null} width={700} + style={{ borderRadius: '16px' }} + styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} >
- {/* 动态生成表单字段 */} {deviceFields.map(field => { - // 根据字段类型生成表单控件 let control = null; switch (field.fieldType) { case 'text': case 'string': - control = ; + control = ; break; case 'number': - control = ; + control = ; break; case 'boolean': control = ; break; case 'date': - control = ; + control = ; break; case 'textarea': - control = ; + control = ; break; case 'select': - // 特殊处理机柜选择(仍然使用单选) if (field.fieldName === 'rackId') { control = ( - {racks.map(rack => (
- {/* 导入设备模态框 */} - {/* 字段配置模态框 */} + + 字段配置 + + } open={fieldConfigModalVisible} onCancel={() => setFieldConfigModalVisible(false)} footer={null} width={600} + styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} >
- {/* 将字段分为多列显示 */}
{deviceFields.map(field => ( - - - + + + + + 导入设备 +
+ } open={importModalVisible} onCancel={() => { setImportModalVisible(false); @@ -1081,15 +1194,16 @@ function DeviceManagement() { footer={null} width={600} destroyOnHidden + styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} > {!isImporting && !importResult ? (
-

请上传CSV格式的设备数据文件

-

支持的编码格式:GBK

+

请上传CSV格式的设备数据文件

+

支持的编码格式:GBK

-
-

CSV文件格式要求:

-
    +
    +

    CSV文件格式要求:

    +
    • 必填字段:设备ID、设备名称、设备类型、型号、序列号、所在机柜ID、位置(U)、高度(U)、功率(W)、状态、购买日期、保修到期
    • 可选字段:IP地址、描述
    • 设备类型:server(服务器)、switch(交换机)、router(路由器)、storage(存储设备)
    • @@ -1098,13 +1212,13 @@ function DeviceManagement() {
    -
    +
    - - 包含示例数据的CSV模板文件 + 包含示例数据的CSV模板文件
    -
    ) : (
    -

    正在导入数据...

    - +

    正在导入数据...

    + {importResult && importResult.statistics && (
    -

    导入完成:

    -

    总记录数:{importResult.statistics.total || 0}

    -

    成功:{importResult.statistics.success || 0}

    -

    失败:{importResult.statistics.failed || 0}

    - {/* 显示导入失败记录详情表格 */} +

    导入完成:

    +
    +
    +
    {importResult.statistics.total || 0}
    +
    总记录数
    +
    +
    +
    {importResult.statistics.success || 0}
    +
    成功
    +
    +
    +
    {importResult.statistics.failed || 0}
    +
    失败
    +
    +
    {(() => { - // 检查是否有错误记录 const errors = importResult.statistics?.errors; const hasErrors = Array.isArray(errors) && errors.length > 0; - - // 即使没有errors字段,如果failed数量大于0,也应该提示用户 const hasFailedRecords = importResult.statistics?.failed > 0; return (hasErrors || hasFailedRecords) && ( -
    -

    失败记录详情:

    +
    +

    失败记录详情:

    {hasErrors ? (
@@ -1173,7 +1294,7 @@ function DeviceManagement() { setImportResult(null); setIsImporting(false); fetchDevices(); - }} style={{ marginTop: 20 }}> + }} style={{ marginTop: '20px', ...primaryButtonStyle, height: '40px' }}> 确定 @@ -1182,9 +1303,13 @@ function DeviceManagement() { )} - {/* 设备详情模态框 */} + + 设备详情 + + } open={detailModalVisible} onCancel={() => { setDetailModalVisible(false); @@ -1194,34 +1319,43 @@ function DeviceManagement() { , ]} width={800} destroyOnHidden + styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }} > {selectedDevice && (
- {/* 基本信息区域 */} - + + + 基本信息 + + } + style={{ borderRadius: '12px', border: '1px solid #f0f0f0' }} + >
- - {selectedDevice.deviceId || '-'} + + {selectedDevice.deviceId || '-'}
- - {selectedDevice.name || '-'} + + {selectedDevice.name || '-'}
- + {selectedDevice.type ? ( @@ -1232,68 +1366,68 @@ function DeviceManagement() {
- - {selectedDevice.model || '-'} + + {selectedDevice.model || '-'}
- - {selectedDevice.serialNumber || '-'} + + {selectedDevice.serialNumber || '-'}
- - {selectedDevice.ipAddress || '-'} + + {selectedDevice.ipAddress || '-'}
- - {selectedDevice.Rack?.Room?.name || '-'} + + {selectedDevice.Rack?.Room?.name || '-'}
- - {selectedDevice.Rack?.name || '-'} + + {selectedDevice.Rack?.name || '-'}
- - {selectedDevice.position || '-'} + + {selectedDevice.position || '-'}
- - {selectedDevice.height || '-'} + + {selectedDevice.height || '-'}
- - {selectedDevice.power || '-'} + + {selectedDevice.power || '-'}
- + {selectedDevice.status ? statusMap[selectedDevice.status]?.text || selectedDevice.status : '-'}
- - + + {selectedDevice.purchaseDate ? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN') : '-'}
- + {selectedDevice.warrantyExpiry ? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN') : '-'}
{selectedDevice.description && ( -
- -
+
+ +
{selectedDevice.description}
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 88fc8c9..c8290f3 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,6 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { Form, Input, Button, Card, message, Typography, Tabs, Divider, Space, Modal, Alert } from 'antd'; -import { UserOutlined, LockOutlined, MailOutlined, PhoneOutlined, SafetyCertificateOutlined, RobotOutlined } from '@ant-design/icons'; +import { Form, Input, Button, Card, message, Typography, Divider, Space, Alert } from 'antd'; +import { + UserOutlined, + LockOutlined, + MailOutlined, + PhoneOutlined, + SafetyCertificateOutlined, + RobotOutlined +} from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; @@ -82,69 +89,131 @@ const Login = () => { display: 'flex', justifyContent: 'center', alignItems: 'center', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - padding: '24px' + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)', + padding: '24px', + position: 'relative', + overflow: 'hidden' + }; + + const backgroundDecorationStyle = { + position: 'absolute', + borderRadius: '50%', + filter: 'blur(80px)', + opacity: '0.3' }; const cardStyle = { width: '100%', - maxWidth: isFirstUser ? 450 : 400, - borderRadius: '16px', - boxShadow: '0 20px 60px rgba(0,0,0,0.3)' + maxWidth: isFirstUser ? 480 : 420, + borderRadius: '20px', + boxShadow: '0 20px 60px rgba(0,0,0,0.25), 0 8px 20px rgba(0,0,0,0.15)', + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(20px)', + border: '1px solid rgba(255, 255, 255, 0.3)' }; const headerStyle = { textAlign: 'center', - marginBottom: '32px' + marginBottom: '32px', + paddingTop: '8px' + }; + + const iconContainerStyle = { + width: '80px', + height: '80px', + borderRadius: '20px', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + margin: '0 auto 20px', + boxShadow: '0 8px 24px rgba(102, 126, 234, 0.4)' }; const titleStyle = { - fontSize: '28px', + fontSize: '26px', fontWeight: '700', - color: '#1a1a2e', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', marginBottom: '8px' }; const subtitleStyle = { fontSize: '14px', - color: '#666' + color: '#8c8c8c' }; const formStyle = { marginTop: '24px' }; + const inputStyle = { + borderRadius: '8px', + height: '48px', + border: '1px solid #e8e8e8' + }; + const submitButtonStyle = { width: '100%', height: '48px', fontSize: '16px', fontWeight: '600', - borderRadius: '8px' + borderRadius: '8px', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + border: 'none', + boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4)', + transition: 'all 0.3s ease' }; const footerStyle = { textAlign: 'center', - marginTop: '24px' + marginTop: '24px', + paddingBottom: '16px' + }; + + const toggleButtonStyle = { + color: '#667eea', + fontWeight: '500', + padding: '4px 8px', + borderRadius: '4px', + transition: 'all 0.3s ease' + }; + + const inputPrefixStyle = { + color: '#667eea', + fontSize: '18px' }; return (
+
+
+
-
- +
+
{isFirstUser ? '创建管理员账户' : 'IDC设备管理系统'} - {isFirstUser ? '首次使用,请创建系统管理员账户' : '请登录您的账户'} + {isFirstUser ? '首次使用,请创建系统管理员账户' : '安全登录您的账户'}
@@ -154,7 +223,7 @@ const Login = () => { description="您是第一个用户,系统将自动为您分配管理员权限。" type="success" showIcon - style={{ marginBottom: '24px' }} + style={{ marginBottom: '24px', borderRadius: '8px' }} /> )} @@ -174,9 +243,10 @@ const Login = () => { { pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' } ]} > - } - placeholder="用户名" + } + placeholder="用户名" + style={inputStyle} /> @@ -184,9 +254,10 @@ const Login = () => { name="realName" rules={[{ required: true, message: '请输入真实姓名' }]} > - } - placeholder="真实姓名" + } + placeholder="真实姓名" + style={inputStyle} /> @@ -197,18 +268,20 @@ const Login = () => { { type: 'email', message: '请输入有效的邮箱地址' } ]} > - } - placeholder="邮箱" + } + placeholder="邮箱" + style={inputStyle} /> - } - placeholder="手机号(可选)" + } + placeholder="手机号(可选)" + style={inputStyle} /> @@ -219,9 +292,10 @@ const Login = () => { { min: 6, message: '密码长度不能少于6个字符' } ]} > - } - placeholder="密码" + } + placeholder="密码" + style={inputStyle} /> @@ -240,9 +314,10 @@ const Login = () => { }), ]} > - } - placeholder="确认密码" + } + placeholder="确认密码" + style={inputStyle} /> @@ -252,9 +327,10 @@ const Login = () => { name="username" rules={[{ required: true, message: '请输入用户名' }]} > - } - placeholder="用户名" + } + placeholder="用户名" + style={inputStyle} /> @@ -262,18 +338,19 @@ const Login = () => { name="password" rules={[{ required: true, message: '请输入密码' }]} > - } - placeholder="密码" + } + placeholder="密码" + style={inputStyle} /> )} - - - + - }> +
+ +
'table-row'} /> + + {editingRack ? '编辑机柜' : '添加机柜'} + + } open={modalVisible} onCancel={handleCancel} footer={null} width={600} + destroyOnClose + styles={{ + body: { padding: '24px' }, + header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' } + }} + style={{ borderRadius: '16px', overflow: 'hidden' }} >
- + - + - {rooms.map(room => ( - + - - + - - + @@ -393,7 +502,12 @@ function RackManagement() { {/* 导入机柜模态框 */} + + 导入机柜 + + } open={importModalVisible} onCancel={() => { setImportModalVisible(false); @@ -404,26 +518,64 @@ function RackManagement() { footer={null} width={600} destroyOnClose + styles={{ + body: { padding: '24px' }, + header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' } + }} + style={{ borderRadius: '16px', overflow: 'hidden' }} > {!isImporting && !importResult ? (
-

请上传XLSX格式的机柜数据文件

-

支持的编码格式:UTF-8

+
+

+ 请上传XLSX格式的机柜数据文件 +

+

+ 支持的编码格式:UTF-8 +

+
-
-

Excel文件格式要求:

-
    +
    +

    + Excel文件格式要求: +

    +
    • 必填字段:机柜ID、机柜名称、所属机房名称、高度(U)、最大功率(W)、状态
    • 状态值:active(在用)、maintenance(维护中)、inactive(停用)
    • 高度和功率必须是数字格式
    -
    - - 包含示例数据的XLSX模板文件 + + 包含示例数据的XLSX模板文件 +
    -
) : ( -
-

正在导入数据...

- +
+

+ {isImporting ? '正在导入数据...' : '导入完成'} +

+ {importResult && ( -
-

导入完成:

-

总记录数:{importResult.total || 0}

-

成功:{importResult.imported || 0}

-

重复:{importResult.duplicates || 0}

+
+

+ 导入结果: +

+

+ ✓ 总记录数:{importResult.total || 0} +

+

+ ✓ 成功:{importResult.imported || 0} +

+

0 ? '#faad14' : '#52c41a' }}> + {importResult.duplicates > 0 ? '⚠' : '✓'} 重复:{importResult.duplicates || 0} +

{importResult.details && importResult.details.length > 0 && ( -
-

导入失败记录:

-
    +
    +

    + 导入失败记录: +

    +
      {importResult.details.map((item, index) => ( -
    • +
    • 第 {item.row} 行
        {item.errors.map((err, errIndex) => ( -
      • {err}
      • +
      • {err}
      • ))}
    • @@ -465,21 +658,21 @@ function RackManagement() {
    )} -
)} +
)} diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx index b21add4..793281c 100644 --- a/frontend/src/pages/RackVisualization.jsx +++ b/frontend/src/pages/RackVisualization.jsx @@ -61,8 +61,10 @@ const getDeviceColor = (deviceType) => { const getDeviceStatusColor = (status) => { const statusColorMap = { 'normal': '#10b981', + 'running': '#10b981', 'warning': '#f59e0b', 'error': '#ef4444', + 'fault': '#ef4444', 'offline': '#6b7280', 'maintenance': '#3b82f6', undefined: '#3b82f6', @@ -71,6 +73,85 @@ const getDeviceStatusColor = (status) => { return statusColorMap[status] || '#3b82f6'; }; +const getStatusTheme = (status) => { + const themeMap = { + 'normal': { + bgGradient: 'linear-gradient(180deg, #059669 0%, #047857 50%, #065f46 100%)', + borderColor: '#10b981', + topBorderColor: '#34d399', + glowColor: 'rgba(16, 185, 129, 0.4)', + shadowColor: 'rgba(16, 185, 129, 0.3)', + iconColor: '#10b981', + label: '正常' + }, + 'running': { + bgGradient: 'linear-gradient(180deg, #059669 0%, #047857 50%, #065f46 100%)', + borderColor: '#10b981', + topBorderColor: '#34d399', + glowColor: 'rgba(16, 185, 129, 0.4)', + shadowColor: 'rgba(16, 185, 129, 0.3)', + iconColor: '#10b981', + label: '运行中' + }, + 'warning': { + bgGradient: 'linear-gradient(180deg, #d97706 0%, #b45309 50%, #92400e 100%)', + borderColor: '#f59e0b', + topBorderColor: '#fbbf24', + glowColor: 'rgba(245, 158, 11, 0.4)', + shadowColor: 'rgba(245, 158, 11, 0.3)', + iconColor: '#f59e0b', + label: '警告' + }, + 'error': { + bgGradient: 'linear-gradient(180deg, #dc2626 0%, #b91c1c 50%, #991b1b 100%)', + borderColor: '#ef4444', + topBorderColor: '#f87171', + glowColor: 'rgba(239, 68, 68, 0.5)', + shadowColor: 'rgba(239, 68, 68, 0.4)', + iconColor: '#ef4444', + label: '故障' + }, + 'fault': { + bgGradient: 'linear-gradient(180deg, #dc2626 0%, #b91c1c 50%, #991b1b 100%)', + borderColor: '#ef4444', + topBorderColor: '#f87171', + glowColor: 'rgba(239, 68, 68, 0.5)', + shadowColor: 'rgba(239, 68, 68, 0.4)', + iconColor: '#ef4444', + label: '故障' + }, + 'offline': { + bgGradient: 'linear-gradient(180deg, #4b5563 0%, #374151 50%, #1f2937 100%)', + borderColor: '#6b7280', + topBorderColor: '#9ca3af', + glowColor: 'rgba(107, 114, 128, 0.2)', + shadowColor: 'rgba(0, 0, 0, 0.2)', + iconColor: '#9ca3af', + label: '离线' + }, + 'maintenance': { + bgGradient: 'linear-gradient(180deg, #2563eb 0%, #1d4ed8 50%, #1e40af 100%)', + borderColor: '#3b82f6', + topBorderColor: '#60a5fa', + glowColor: 'rgba(59, 130, 246, 0.4)', + shadowColor: 'rgba(59, 130, 246, 0.3)', + iconColor: '#3b82f6', + label: '维护中' + }, + 'default': { + bgGradient: 'linear-gradient(180deg, #3d4451 0%, #2d3139 50%, #252930 100%)', + borderColor: '#4a5568', + topBorderColor: '#565c6b', + glowColor: 'rgba(56, 189, 248, 0.2)', + shadowColor: 'rgba(0, 0, 0, 0.3)', + iconColor: '#38bdf8', + label: '未知' + } + }; + + return themeMap[status] || themeMap['default']; +}; + const getDeviceTypeTheme = (type) => { const themeMap = { 'server': { @@ -317,6 +398,56 @@ class ErrorBoundary extends React.Component { } function RackVisualization() { + const pageHeaderStyle = { + marginBottom: '24px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap', + gap: '16px' + }; + + const titleStyle = { + fontSize: '24px', + fontWeight: '700', + margin: 0, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + backgroundClip: 'text' + }; + + const primaryButtonStyle = { + height: '40px', + borderRadius: '8px', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + border: 'none', + boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)', + fontWeight: '500', + transition: 'all 0.3s ease' + }; + + const secondaryButtonStyle = { + height: '40px', + borderRadius: '8px', + border: '1px solid #e8e8e8', + transition: 'all 0.3s ease' + }; + + const cardStyle = { + borderRadius: '16px', + border: 'none', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', + overflow: 'hidden' + }; + + const searchCardStyle = { + borderRadius: '12px', + border: 'none', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)', + marginBottom: '20px' + }; + const [racks, setRacks] = useState([]); const [selectedRack, setSelectedRack] = useState(null); const [selectedRoom, setSelectedRoom] = useState(null); @@ -1035,96 +1166,98 @@ function RackVisualization() { // 无需额外的初始化,CSS 3D变换直接在JSX中实现 return ( -
- - handleSearch(e.target.value)} - onPressEnter={(e) => handleSearch(e.target.value)} - style={{ width: 220 }} - allowClear - prefix={} - suffix={ - searchMatchCount > 0 ? ( - - ) : null - } +
+
+

+ + 机柜可视化 +

+ + handleSearch(e.target.value)} + onPressEnter={(e) => handleSearch(e.target.value)} + style={{ width: 220, height: '40px', borderRadius: '8px' }} + allowClear + prefix={} + suffix={ + searchMatchCount > 0 ? ( + + ) : null + } + /> + {searchKeyword && ( + - - - } - > - {searchKeyword && ( + )} + + + + + +
+ + {searchKeyword && ( +
0 ? 'linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%)' : '#fff2f0', - border: `1px solid ${searchResults.length > 0 ? '#b7eb8f' : '#ffccc7'}`, - borderRadius: '6px', display: 'flex', alignItems: 'center', - gap: '12px' + gap: '12px', + flexWrap: 'wrap' }}> 0 ? '#52c41a' : '#ff4d4f' }} /> - 0 ? '#135200' : '#cf1322' }}> + 0 ? '#135200' : '#cf1322', margin: 0 }}> {searchResults.length > 0 ? `找到 ${searchResults.length} 个设备` : searching ? '搜索中...' : '未找到匹配的设备'} @@ -1143,7 +1276,6 @@ function RackVisualization() { type={highlightedDevice === result.deviceId ? 'primary' : 'default'} icon={} onClick={() => { - // 如果设备不在当前机柜,跳转到对应机柜 if (selectedRack && result.rackId !== selectedRack.rackId) { const targetRack = racks.find(r => r.rackId === result.rackId); if (targetRack) { @@ -1161,8 +1293,10 @@ function RackVisualization() { setHighlightedDevice(result.deviceId); }} style={{ - borderColor: highlightedDevice === result.deviceId ? '#1890ff' : undefined, - background: highlightedDevice === result.deviceId ? '#1890ff' : undefined + borderColor: highlightedDevice === result.deviceId ? '#667eea' : undefined, + background: highlightedDevice === result.deviceId ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : undefined, + height: '32px', + borderRadius: '6px' }} > @@ -1185,37 +1319,40 @@ function RackVisualization() { )}
- )} +
+ )} + +
- - - - + + + + + {backgroundType === 'image' && ( + + { + const image = e.target.value; + setBackgroundImage(image); + await saveBackgroundSettings(backgroundType, image, backgroundSize); }} - > - - - - {backgroundType === 'image' && ( - - { - const image = e.target.value; - setBackgroundImage(image); - await saveBackgroundSettings(backgroundType, image, backgroundSize); - }} - value={backgroundImage || ''} - /> + value={backgroundImage || ''} + /> - + {backgroundImage && ( - @@ -1288,9 +1422,10 @@ function RackVisualization() { display: 'flex', justifyContent: 'center', alignItems: 'center', - backgroundColor: '#f5f5f5', + backgroundColor: '#fafafa', fontSize: '16px', - color: '#666' + color: '#666', + borderRadius: '12px' }}> 加载机柜数据中...
@@ -1299,23 +1434,23 @@ function RackVisualization() { className="rack-visualization-container" style={{ width: '100%', - height: 'calc(100vh - 200px)', + height: 'calc(100vh - 280px)', display: 'flex', flexDirection: 'column', justifyContent: 'flex-start', perspective: '1000px', overflow: 'auto', - backgroundColor: '#f5f5f5', + backgroundColor: '#fafafa', background: backgroundType === 'image' && backgroundImage ? `url(${backgroundImage})` - : '#f8fffe', + : 'linear-gradient(180deg, #f8fffe 0%, #f0fdf4 50%, #e6fffa 100%)', backgroundSize: backgroundType === 'image' ? backgroundSize : 'auto', backgroundPosition: 'center', backgroundRepeat: 'no-repeat', position: 'relative', - borderRadius: '8px', + borderRadius: '12px', border: '1px solid #e8e8e8', - boxShadow: '0 2px 8px rgba(0,0,0,0.08)', + boxShadow: '0 2px 8px rgba(0,0,0,0.06)', transition: 'all 0.3s ease', padding: '20px', boxSizing: 'border-box', @@ -1560,6 +1695,10 @@ function RackVisualization() { // 检查是否是高亮的设备 const isHighlighted = highlightedDevice === deviceId; + // 获取状态主题 + const statusTheme = getStatusTheme(device?.status); + const statusColor = getDeviceStatusColor(device?.status); + return (
{ const isOneU = (device?.height || 1) === 1; @@ -1611,11 +1752,11 @@ function RackVisualization() { } e.currentTarget.style.transform = 'scale(1.008)'; e.currentTarget.style.boxShadow = ` - 0 4px 12px rgba(56, 189, 248, 0.3), + 0 4px 12px ${statusTheme.shadowColor}, 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.15) `; - e.currentTarget.style.borderColor = '#38bdf8'; + e.currentTarget.style.borderColor = statusTheme.topBorderColor; const rect = e.currentTarget.getBoundingClientRect(); setTooltipPosition({ x: rect.right + 5, y: rect.top + rect.height / 2 }); @@ -1635,24 +1776,25 @@ function RackVisualization() { inset 0 1px 0 rgba(255,255,255,0.1), inset 0 -1px 0 rgba(0,0,0,0.1) `; - e.currentTarget.style.borderColor = '#4a5568'; + e.currentTarget.style.borderColor = statusTheme.borderColor; setDeviceTooltip(null); }} >
{/* 左侧状态指示区域 - 增强版 */}
{/* 设备类型标识 */}
diff --git a/frontend/src/pages/RoomManagement.jsx b/frontend/src/pages/RoomManagement.jsx index a5eb3d2..d6ce4ad 100644 --- a/frontend/src/pages/RoomManagement.jsx +++ b/frontend/src/pages/RoomManagement.jsx @@ -176,30 +176,144 @@ function RoomManagement() { }, ]; + const pageHeaderStyle = { + marginBottom: '24px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap', + gap: '16px' + }; + + const titleStyle = { + fontSize: '24px', + fontWeight: '700', + margin: 0, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + backgroundClip: 'text' + }; + + const cardStyle = { + borderRadius: '16px', + border: 'none', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', + overflow: 'hidden' + }; + + const cardHeadStyle = { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)' + }; + + const tableStyle = { + borderRadius: '12px', + overflow: 'hidden' + }; + + const statusTagStyle = (color) => ({ + padding: '4px 12px', + borderRadius: '20px', + fontSize: '12px', + fontWeight: '500', + border: 'none', + background: color === 'green' ? '#f6ffed' : color === 'orange' ? '#fff7e6' : '#f5f5f5', + color: color === 'green' ? '#52c41a' : color === 'orange' ? '#faad14' : '#8c8c8c' + }); + + const primaryButtonStyle = { + height: '40px', + borderRadius: '8px', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + border: 'none', + boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)', + fontWeight: '500', + transition: 'all 0.3s ease' + }; + + const actionButtonStyle = { + height: '32px', + borderRadius: '6px', + border: '1px solid #e8e8e8', + transition: 'all 0.3s ease' + }; + return ( -
- - + - }> +
+ +
`共 ${total} 条记录` + }} + style={tableStyle} + rowClassName={() => 'table-row'} /> - + + {editingRoom ? '编辑机房' : '添加机房'} + + } open={modalVisible} onCancel={handleCancel} footer={null} width={600} + destroyOnClose + styles={{ + body: { padding: '24px' }, + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + marginBottom: '0' + } + }} + style={{ + borderRadius: '16px', + overflow: 'hidden' + }} > - + - + - + - + - + - @@ -262,13 +376,27 @@ function RoomManagement() { name="description" label="描述" > - + - + - - + diff --git a/frontend/src/pages/UserManagement.jsx b/frontend/src/pages/UserManagement.jsx index cce06fd..396f28e 100644 --- a/frontend/src/pages/UserManagement.jsx +++ b/frontend/src/pages/UserManagement.jsx @@ -323,28 +323,108 @@ const UserManagement = () => { marginBottom: '24px', display: 'flex', justifyContent: 'space-between', - alignItems: 'center' + alignItems: 'center', + flexWrap: 'wrap', + gap: '16px' }; const titleStyle = { - fontSize: '20px', - fontWeight: '600', - margin: 0 + fontSize: '24px', + fontWeight: '700', + margin: 0, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + backgroundClip: 'text' + }; + + const cardStyle = { + borderRadius: '16px', + border: 'none', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', + overflow: 'hidden' + }; + + const cardHeadStyle = { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)' + }; + + const primaryButtonStyle = { + height: '40px', + borderRadius: '8px', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + border: 'none', + boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)', + fontWeight: '500', + transition: 'all 0.3s ease' + }; + + const secondaryButtonStyle = { + height: '40px', + borderRadius: '8px', + border: '1px solid #e8e8e8', + transition: 'all 0.3s ease' + }; + + const actionButtonStyle = { + height: '32px', + borderRadius: '6px', + border: '1px solid #e8e8e8', + transition: 'all 0.3s ease' + }; + + const modalHeaderStyle = { + display: 'flex', + alignItems: 'center', + gap: '8px', + fontSize: '18px', + fontWeight: '600' + }; + + const modalHeaderAccent = { + width: '4px', + height: '20px', + background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)', + borderRadius: '2px' + }; + + const avatarModalStyle = { + textAlign: 'center', + padding: '20px 0' + }; + + const avatarWrapperStyle = { + marginBottom: '24px', + position: 'relative', + display: 'inline-block' }; return ( -
+

用户管理

- - - +
- +
{ onChange={(newPagination) => { setPagination(prev => ({ ...prev, ...newPagination })); }} + rowClassName={() => 'table-row'} /> + + {editingUser ? '编辑用户' : '添加用户'} + + } open={modalVisible} onCancel={() => setModalVisible(false)} footer={null} width={500} + destroyOnClose + styles={{ + body: { padding: '24px' }, + header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' } + }} + style={{ borderRadius: '16px', overflow: 'hidden' }} > { { min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' } ]} > - + { label="真实姓名" rules={[{ required: true, message: '请输入真实姓名' }]} > - + { { type: 'email', message: '请输入有效的邮箱地址' } ]} > - + - + { label="角色" rules={[{ required: true, message: '请选择角色' }]} > - {roles.map(role => ( )} - @@ -452,14 +544,29 @@ const UserManagement = () => { { min: 6, message: '密码长度不能少于6个字符' } ]} > - + )} - + - - + @@ -468,11 +575,22 @@ const UserManagement = () => { + + {`重置密码 - ${passwordUser?.username}`} + + } open={passwordModalVisible} onCancel={() => setPasswordModalVisible(false)} footer={null} width={400} + destroyOnClose + styles={{ + body: { padding: '24px' }, + header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' } + }} + style={{ borderRadius: '16px', overflow: 'hidden' }} > { { min: 6, message: '密码长度不能少于6个字符' } ]} > - + - + - - + @@ -503,14 +636,25 @@ const UserManagement = () => { + + {`设置头像 - ${avatarUser?.username}`} + + } open={avatarModalVisible} onCancel={() => setAvatarModalVisible(false)} footer={null} width={400} + destroyOnClose + styles={{ + body: { padding: '24px' }, + header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' } + }} + style={{ borderRadius: '16px', overflow: 'hidden' }} > -
-
+
+
{ src={getAvatarUrl(avatarUser)} style={{ backgroundColor: avatarUser?.avatar ? 'transparent' : '#1890ff', - border: '1px solid #f0f0f0' + border: '1px solid #f0f0f0', + cursor: 'pointer' }} /> @@ -539,6 +684,10 @@ const UserManagement = () => { onClick={() => fileInputRef.current?.click()} loading={uploadLoading} block + style={{ + ...primaryButtonStyle, + height: '44px' + }} > {avatarUser?.avatar ? '更换头像' : '上传头像'} @@ -549,13 +698,14 @@ const UserManagement = () => { icon={} onClick={handleAvatarDelete} block + style={{ borderRadius: '8px', height: '44px' }} > 删除头像 )} -
+
支持 JPG、PNG、GIF、WebP 格式,大小不超过 5MB