diff --git a/oms_web/oms_web_new/src/components/ConfirmDialog/ConfirmDialog.jsx b/oms_web/oms_web_new/src/components/ConfirmDialog/ConfirmDialog.jsx new file mode 100644 index 00000000..d7095b01 --- /dev/null +++ b/oms_web/oms_web_new/src/components/ConfirmDialog/ConfirmDialog.jsx @@ -0,0 +1,138 @@ +import { Modal } from 'antd' +import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons' + +/** + * 标准确认对话框组件 + * @param {Object} options - 对话框配置 + * @param {string} options.title - 标题 + * @param {string|ReactNode} options.content - 内容 + * @param {string} options.okText - 确认按钮文字 + * @param {string} options.cancelText - 取消按钮文字 + * @param {string} options.type - 类型: 'warning', 'danger', 'info' + * @param {Function} options.onOk - 确认回调 + * @param {Function} options.onCancel - 取消回调 + */ +const ConfirmDialog = { + /** + * 显示删除确认对话框(单个项目) + */ + delete: ({ title = '确认删除', itemName, itemInfo, onOk, onCancel }) => { + Modal.confirm({ + title, + content: ( +
+

您确定要删除以下项目吗?

+
+

{itemName}

+ {itemInfo && ( +

{itemInfo}

+ )} +
+

+ 此操作不可恢复,请谨慎操作! +

+
+ ), + okText: '确认删除', + cancelText: '取消', + okType: 'danger', + centered: true, + icon: , + onOk, + onCancel, + }) + }, + + /** + * 显示批量删除确认对话框 + */ + batchDelete: ({ count, items, onOk, onCancel }) => { + Modal.confirm({ + title: '批量删除确认', + content: ( +
+

您确定要删除选中的 {count} 个项目吗?

+
+ {items.map((item, index) => ( +
+ {item.name} + {item.info && ( + + ({item.info}) + + )} +
+ ))} +
+

+ 此操作不可恢复,请谨慎操作! +

+
+ ), + okText: '确认删除', + cancelText: '取消', + okType: 'danger', + centered: true, + icon: , + onOk, + onCancel, + }) + }, + + /** + * 显示警告确认对话框 + */ + warning: ({ title, content, okText = '确定', cancelText = '取消', onOk, onCancel }) => { + Modal.confirm({ + title, + content, + okText, + cancelText, + centered: true, + icon: , + onOk, + onCancel, + }) + }, + + /** + * 显示通用确认对话框 + */ + confirm: ({ + title, + content, + okText = '确定', + cancelText = '取消', + okType = 'primary', + onOk, + onCancel, + }) => { + Modal.confirm({ + title, + content, + okText, + cancelText, + okType, + centered: true, + onOk, + onCancel, + }) + }, +} + +export default ConfirmDialog diff --git a/oms_web/oms_web_new/src/components/DetailDrawer/DetailDrawer.css b/oms_web/oms_web_new/src/components/DetailDrawer/DetailDrawer.css new file mode 100644 index 00000000..32bf54d7 --- /dev/null +++ b/oms_web/oms_web_new/src/components/DetailDrawer/DetailDrawer.css @@ -0,0 +1,120 @@ +/* 详情抽屉容器 */ +.detail-drawer-content { + height: 100%; + display: flex; + flex-direction: column; +} + +/* 顶部信息区域 - 固定不滚动 */ +.detail-drawer-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + background: #fafafa; + border-bottom: 1px solid #f0f0f0; + flex-shrink: 0; +} + +.detail-drawer-header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.detail-drawer-close-button { + font-size: 18px; + color: #666; +} + +.detail-drawer-close-button:hover { + color: #1677ff; +} + +.detail-drawer-header-info { + display: flex; + align-items: center; + gap: 12px; +} + +.detail-drawer-title-icon { + font-size: 18px; + color: #1677ff; +} + +.detail-drawer-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: rgba(0, 0, 0, 0.88); +} + +.detail-drawer-badge { + display: flex; + align-items: center; +} + +.detail-drawer-header-right { + flex: 1; + display: flex; + justify-content: flex-end; +} + +/* 可滚动内容区域 */ +.detail-drawer-scrollable-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; +} + +/* 标签页区域 */ +.detail-drawer-tabs { + background: #ffffff; + padding-top: 16px; + padding-left: 12px; + min-height: 400px; +} + +.detail-drawer-tabs :global(.ant-tabs) { + height: 100%; +} + +.detail-drawer-tabs :global(.ant-tabs-content-holder) { + overflow: visible; +} + +.detail-drawer-tabs :global(.ant-tabs-nav) { + padding: 0; + margin: 0 24px; + margin-bottom: 0; + background: transparent; +} + +.detail-drawer-tabs :global(.ant-tabs-nav::before) { + border-bottom: 1px solid #f0f0f0; +} + +.detail-drawer-tabs :global(.ant-tabs-tab) { + padding: 12px 0; + margin: 0 32px 0 0; + font-size: 14px; + font-weight: 500; +} + +.detail-drawer-tabs :global(.ant-tabs-tab:first-child) { + margin-left: 0; +} + +.detail-drawer-tabs :global(.ant-tabs-tab-active .ant-tabs-tab-btn) { + color: #d946ef; +} + +.detail-drawer-tabs :global(.ant-tabs-ink-bar) { + background: #d946ef; + height: 3px; +} + +.detail-drawer-tab-content { + padding: 24px; + background: #ffffff; +} diff --git a/oms_web/oms_web_new/src/components/DetailDrawer/DetailDrawer.jsx b/oms_web/oms_web_new/src/components/DetailDrawer/DetailDrawer.jsx new file mode 100644 index 00000000..2ef0eab3 --- /dev/null +++ b/oms_web/oms_web_new/src/components/DetailDrawer/DetailDrawer.jsx @@ -0,0 +1,97 @@ +import { Drawer, Button, Space, Tabs } from 'antd' +import { CloseOutlined } from '@ant-design/icons' +import './DetailDrawer.css' + +/** + * 详情抽屉组件 + * @param {Object} props + * @param {boolean} props.visible - 是否显示抽屉 + * @param {Function} props.onClose - 关闭回调 + * @param {Object} props.title - 标题配置 + * @param {string} props.title.text - 标题文本 + * @param {ReactNode} props.title.badge - 状态徽标(可选) + * @param {ReactNode} props.title.icon - 图标(可选) + * @param {Array} props.headerActions - 顶部操作按钮 + * @param {number} props.width - 抽屉宽度 + * @param {ReactNode} props.children - 主要内容 + * @param {Array} props.tabs - 标签页配置(可选) + */ +function DetailDrawer({ + visible, + onClose, + title, + headerActions = [], + width = 1080, + children, + tabs, +}) { + return ( + +
+ {/* 顶部标题栏 - 固定不滚动 */} +
+
+
+
+ + {headerActions.map((action) => ( + + ))} + +
+
+ + {/* 可滚动内容区域 */} +
+ {children} + + {/* 可选的标签页区域 */} + {tabs && tabs.length > 0 && ( +
+ ({ + key: tab.key, + label: tab.label, + children:
{tab.content}
, + }))} + /> +
+ )} +
+
+
+ ) +} + +export default DetailDrawer diff --git a/oms_web/oms_web_new/src/components/InfoPanel/InfoPanel.css b/oms_web/oms_web_new/src/components/InfoPanel/InfoPanel.css new file mode 100644 index 00000000..0593f3c6 --- /dev/null +++ b/oms_web/oms_web_new/src/components/InfoPanel/InfoPanel.css @@ -0,0 +1,94 @@ +/* 信息面板 */ +.info-panel { + padding: 6px 8px; + background: #ffffff; +} + +/* 信息区域容器 */ +.info-panel > :global(.ant-row) { + padding: 32px; + background: #ffffff; + border-bottom: 1px solid #f0f0f0; +} + +.info-panel-item { + display: flex; + flex-direction: column; + gap: 5px; + padding: 10px 0; + border-bottom: 1px solid #f0f0f0; + transition: all 0.2s ease; + position: relative; +} + +.info-panel-item:last-child { + border-bottom: none; +} + +/* 添加底部装饰条 */ +.info-panel-item::before { + content: ''; + position: absolute; + left: 3px; + bottom: -1px; + width: 0; + height: 3px; + background: linear-gradient(90deg, #1677ff 0%, #4096ff 100%); + border-radius: 2px; + transition: width 0.3s ease; +} + +.info-panel-item:hover { + background: linear-gradient(90deg, #f0f7ff 0%, transparent 100%); + padding-left: 10px; + padding-right: 16px; + margin-left: -12px; + margin-right: -16px; + border-radius: 8px; + border-bottom-color: transparent; +} + +.info-panel-item:hover::before { + width: 60px; +} + +.info-panel-label { + color: rgba(0, 0, 0, 0.45); + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +} + +.info-panel-value { + color: rgba(0, 0, 0, 0.88); + font-size: 15px; + font-weight: 500; + word-break: break-all; + line-height: 1.6; +} + +/* 操作按钮区 */ +.info-panel-actions { + padding: 24px 32px; + background: linear-gradient(to bottom, #fafafa 0%, #f5f5f5 100%); + border-top: 2px solid #e8e8e8; + position: relative; +} + +/* 操作区域顶部装饰线 */ +.info-panel-actions::before { + content: ''; + position: absolute; + top: -2px; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, #1677ff 0%, transparent 50%, #1677ff 100%); + opacity: 0.3; +} + + + + diff --git a/oms_web/oms_web_new/src/components/InfoPanel/InfoPanel.jsx b/oms_web/oms_web_new/src/components/InfoPanel/InfoPanel.jsx new file mode 100644 index 00000000..c60f2c99 --- /dev/null +++ b/oms_web/oms_web_new/src/components/InfoPanel/InfoPanel.jsx @@ -0,0 +1,58 @@ +import { Row, Col, Space, Button } from 'antd' +import './InfoPanel.css' + +/** + * 信息展示面板组件 + * @param {Object} props + * @param {Object} props.data - 数据源 + * @param {Array} props.fields - 字段配置数组 + * @param {Array} props.actions - 操作按钮配置(可选) + * @param {Array} props.gutter - Grid间距配置 + */ +function InfoPanel({ data, fields = [], actions = [], gutter = [24, 16] }) { + if (!data) { + return null + } + + return ( +
+ + {fields.map((field) => { + const value = data[field.key] + const displayValue = field.render ? field.render(value, data) : value + + return ( + +
+
{field.label}
+
{displayValue}
+
+ + ) + })} +
+ + {/* 可选的操作按钮区 */} + {actions && actions.length > 0 && ( +
+ + {actions.map((action) => ( + + ))} + +
+ )} +
+ ) +} + +export default InfoPanel diff --git a/oms_web/oms_web_new/src/components/ListActionBar/ListActionBar.css b/oms_web/oms_web_new/src/components/ListActionBar/ListActionBar.css new file mode 100644 index 00000000..b1b6fb6e --- /dev/null +++ b/oms_web/oms_web_new/src/components/ListActionBar/ListActionBar.css @@ -0,0 +1,49 @@ +.list-action-bar { + position: sticky; + top: 0; + z-index: 10; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding: 16px; + background: #ffffff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.list-action-bar-left, +.list-action-bar-right { + display: flex; + gap: 12px; + align-items: center; +} + +/* 搜索和筛选组合 */ +.list-action-bar-right :global(.ant-space-compact) { + display: flex; +} + +.list-action-bar-right :global(.ant-space-compact .ant-input-search) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.list-action-bar-right :global(.ant-space-compact > .ant-btn) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* 响应式 */ +@media (max-width: 768px) { + .list-action-bar { + flex-direction: column; + gap: 12px; + align-items: stretch; + } + + .list-action-bar-left, + .list-action-bar-right { + flex-wrap: wrap; + } +} diff --git a/oms_web/oms_web_new/src/components/ListActionBar/ListActionBar.jsx b/oms_web/oms_web_new/src/components/ListActionBar/ListActionBar.jsx new file mode 100644 index 00000000..97c8043f --- /dev/null +++ b/oms_web/oms_web_new/src/components/ListActionBar/ListActionBar.jsx @@ -0,0 +1,86 @@ +import { Button, Input, Space, Popover } from 'antd' +import { ReloadOutlined, FilterOutlined } from '@ant-design/icons' +import './ListActionBar.css' + +const { Search } = Input + +/** + * 列表操作栏组件 + * @param {Object} props + * @param {Array} props.actions - 左侧操作按钮配置数组 + * @param {Object} props.search - 搜索配置 + * @param {Object} props.filter - 高级筛选配置(可选) + * @param {boolean} props.showRefresh - 是否显示刷新按钮 + * @param {Function} props.onRefresh - 刷新回调 + */ +function ListActionBar({ + actions = [], + search, + filter, + showRefresh = false, + onRefresh, +}) { + return ( +
+ {/* 左侧操作按钮区 */} +
+ {actions.map((action) => ( + + ))} +
+ + {/* 右侧搜索筛选区 */} +
+ + search?.onChange?.(e.target.value)} + value={search?.value} + /> + {filter && ( + + + {filter.title || '高级筛选'} +
+ } + trigger="click" + open={filter.visible} + onOpenChange={filter.onVisibleChange} + placement="bottomRight" + overlayClassName="filter-popover" + > + + + )} + + {showRefresh && ( + + )} +
+ + ) +} + +export default ListActionBar diff --git a/oms_web/oms_web_new/src/components/ListTable/ListTable.css b/oms_web/oms_web_new/src/components/ListTable/ListTable.css new file mode 100644 index 00000000..7d967095 --- /dev/null +++ b/oms_web/oms_web_new/src/components/ListTable/ListTable.css @@ -0,0 +1,43 @@ +/* 列表表格容器 */ +.list-table-container { + background: #ffffff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +/* 表格行样式 */ +.list-table-container :global(.ant-table-row) { + cursor: pointer; + transition: all 0.3s; +} + +.list-table-container :global(.ant-table-row:hover) { + background: #f5f5f5; +} + +.list-table-container :global(.ant-table-row.row-selected) { + background: #e6f4ff; +} + +/* 操作列样式 - 重新设计 */ +.list-table-container :global(.ant-table-thead > tr > th:last-child) { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; + color: #ffffff !important; + font-weight: 600; + border-left: 2px solid #e8e8e8; +} + +.list-table-container :global(.ant-table-tbody > tr > td:last-child) { + background: #f8f9ff !important; + border-left: 2px solid #e8e8e8; + box-shadow: -2px 0 4px rgba(0, 0, 0, 0.02); +} + +.list-table-container :global(.ant-table-tbody > tr:hover > td:last-child) { + background: #eef0ff !important; +} + +.list-table-container :global(.ant-table-tbody > tr.row-selected > td:last-child) { + background: #e1e6ff !important; +} diff --git a/oms_web/oms_web_new/src/components/ListTable/ListTable.jsx b/oms_web/oms_web_new/src/components/ListTable/ListTable.jsx new file mode 100644 index 00000000..49bc09fa --- /dev/null +++ b/oms_web/oms_web_new/src/components/ListTable/ListTable.jsx @@ -0,0 +1,67 @@ +import { Table } from 'antd' +import './ListTable.css' + +/** + * 列表表格组件 + * @param {Object} props + * @param {Array} props.columns - 表格列配置 + * @param {Array} props.dataSource - 数据源 + * @param {string} props.rowKey - 行唯一标识字段 + * @param {Array} props.selectedRowKeys - 选中的行 + * @param {Function} props.onSelectionChange - 选择变化回调 + * @param {Object} props.pagination - 分页配置 + * @param {Object} props.scroll - 表格滚动配置 + * @param {Function} props.onRowClick - 行点击回调 + * @param {Object} props.selectedRow - 当前选中的行 + * @param {boolean} props.loading - 加载状态 + * @param {string} props.className - 自定义类名 + */ +function ListTable({ + columns, + dataSource, + rowKey = 'id', + selectedRowKeys = [], + onSelectionChange, + pagination = { + pageSize: 10, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total) => `共 ${total} 条`, + }, + scroll = { x: 1200 }, + onRowClick, + selectedRow, + loading = false, + className = '', +}) { + // 行选择配置 + const rowSelection = { + selectedRowKeys, + onChange: (newSelectedRowKeys) => { + onSelectionChange?.(newSelectedRowKeys) + }, + } + + return ( +
+ ({ + onClick: () => onRowClick?.(record), + className: selectedRow?.[rowKey] === record[rowKey] ? 'row-selected' : '', + })} + /> + + ) +} + +export default ListTable diff --git a/oms_web/oms_web_new/src/components/MainLayout/AppHeader.css b/oms_web/oms_web_new/src/components/MainLayout/AppHeader.css new file mode 100644 index 00000000..9b18ee4c --- /dev/null +++ b/oms_web/oms_web_new/src/components/MainLayout/AppHeader.css @@ -0,0 +1,129 @@ +.app-header { + background: #fff; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); + height: 64px; + border-bottom: 1px solid #f0f0f0; +} + +/* 左侧区域 */ +.header-left { + display: flex; + align-items: center; + gap: 16px; +} + +/* Logo 区域 */ +.header-logo { + display: flex; + align-items: center; + justify-content: center; + width: 168px; + transition: width 0.2s; +} + +.logo-small { + width: 40px; + height: 40px; + border-radius: 8px; + transition: all 0.2s; +} + +.logo-full { + height: 32px; + width: auto; + transition: all 0.2s; +} + +.trigger { + font-size: 18px; + cursor: pointer; + transition: color 0.3s; + padding: 8px; + border-radius: 4px; + color: rgba(0, 0, 0, 0.65); + display: flex; + align-items: center; +} + +.trigger:hover { + color: #b8178d; + background: rgba(184, 23, 141, 0.06); +} + +/* 右侧区域 */ +.header-right { + display: flex; + align-items: center; + gap: 16px; +} + +.header-search { + border-radius: 16px; +} + +.header-actions { + display: flex; + align-items: center; +} + +.header-icon { + font-size: 16px; + color: rgba(0, 0, 0, 0.65); + cursor: pointer; + transition: all 0.3s; + padding: 8px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.header-icon:hover { + color: #b8178d; + background: rgba(184, 23, 141, 0.06); +} + +.header-link { + font-size: 14px; + color: rgba(0, 0, 0, 0.65); + cursor: pointer; + transition: all 0.3s; + padding: 6px 12px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 4px; +} + +.header-link:hover { + color: #b8178d; + background: rgba(184, 23, 141, 0.06); +} + +.user-info { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: all 0.3s; +} + +.user-info:hover { + background: rgba(184, 23, 141, 0.06); +} + +.username { + font-size: 14px; + color: rgba(0, 0, 0, 0.88); + font-weight: 500; +} + +.ml-1 { + margin-left: 4px; +} diff --git a/oms_web/oms_web_new/src/components/MainLayout/AppHeader.jsx b/oms_web/oms_web_new/src/components/MainLayout/AppHeader.jsx new file mode 100644 index 00000000..90bb7f2c --- /dev/null +++ b/oms_web/oms_web_new/src/components/MainLayout/AppHeader.jsx @@ -0,0 +1,128 @@ +import { Layout, Input, Badge, Avatar, Dropdown, Space } from 'antd' +import { useNavigate } from 'react-router-dom' +import { + MenuFoldOutlined, + MenuUnfoldOutlined, + SearchOutlined, + BellOutlined, + QuestionCircleOutlined, + FileTextOutlined, + CustomerServiceOutlined, + UserOutlined, +} from '@ant-design/icons' +import headerMenuData from '../../constants/headerMenuData.json' +import logoFull from '../../assets/logo-full.png' +import './AppHeader.css' + +const { Header } = Layout + +// 图标映射 +const iconMap = { + QuestionCircleOutlined: , + FileTextOutlined: , + CustomerServiceOutlined: , +} + +function AppHeader({ collapsed, onToggle }) { + const navigate = useNavigate() + + // 用户下拉菜单 + const userMenuItems = [ + { + key: 'profile', + label: '个人中心', + }, + { + key: 'settings', + label: '账户设置', + }, + { + type: 'divider', + }, + { + key: 'logout', + label: '退出登录', + }, + ] + + const handleUserMenuClick = ({ key }) => { + if (key === 'logout') { + console.log('退出登录') + } + } + + const handleHeaderMenuClick = (key) => { + console.log('Header menu clicked:', key) + // 如果点击的是设计文档,跳转到文档页面 + if (key === 'docs') { + navigate('/design') + } + } + + return ( +
+ {/* 左侧:Logo + 折叠按钮 */} +
+ {/* Logo 区域 */} +
+ NEX Console +
+ + {/* 折叠按钮 */} +
+ {collapsed ? : } +
+
+ + {/* 右侧:搜索 + 功能按钮 + 用户信息 */} +
+ {/* 搜索框 */} + } + style={{ width: 200 }} + /> + + {/* 功能图标 */} + + {/* 动态渲染 header 菜单 */} + {headerMenuData.map((item) => ( +
handleHeaderMenuClick(item.key)} + > + {iconMap[item.icon]} + {item.label} +
+ ))} + + {/* 消息中心 */} + +
+ +
+
+ + {/* 用户下拉菜单 */} + +
+ } /> + Admin +
+
+
+
+
+ ) +} + +export default AppHeader diff --git a/oms_web/oms_web_new/src/components/MainLayout/AppSider.css b/oms_web/oms_web_new/src/components/MainLayout/AppSider.css new file mode 100644 index 00000000..f4a98aa8 --- /dev/null +++ b/oms_web/oms_web_new/src/components/MainLayout/AppSider.css @@ -0,0 +1,96 @@ +.app-sider { + height: 100%; + overflow: auto; + background: #fafafa; + border-right: 1px solid #f0f0f0; + transition: all 0.2s; +} + +.app-sider::-webkit-scrollbar { + width: 6px; +} + +.app-sider::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.1); + border-radius: 3px; +} + +.app-sider::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.2); +} + +/* 菜单样式 */ +.sider-menu { + border-right: none; + padding-top: 8px; + background: #fafafa; +} + +/* 收起状态下的图标放大 */ +:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-item) { + padding: 0 !important; + display: flex; + align-items: center; + justify-content: center; + height: 56px; + margin: 8px 0; +} + +/* 收起状态下的 SubMenu 样式 */ +:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu) { + padding: 0 !important; +} + +:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu-title) { + padding: 0 !important; + display: flex; + align-items: center; + justify-content: center; + height: 56px; + margin: 8px 0; +} + +:global(.ant-layout-sider-collapsed) .sider-menu :global(.anticon) { + font-size: 24px; + margin: 0; +} + +/* 收起状态下的 Tooltip */ +:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-item-icon) { + font-size: 24px; +} + +:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu-title) :global(.anticon) { + font-size: 24px; + margin: 0; +} + +/* 菜单项徽章 */ +.menu-item-with-badge { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.menu-badge { + font-size: 10px; + height: 18px; + line-height: 18px; + border-radius: 9px; + padding: 0 6px; + margin-left: 8px; +} + +.badge-hot :global(.ant-badge-count) { + background: #ff4d4f; +} + +.badge-new :global(.ant-badge-count) { + background: #52c41a; +} + +/* 收起状态下隐藏徽章 */ +:global(.ant-layout-sider-collapsed) .menu-badge { + display: none; +} diff --git a/oms_web/oms_web_new/src/components/MainLayout/AppSider.jsx b/oms_web/oms_web_new/src/components/MainLayout/AppSider.jsx new file mode 100644 index 00000000..6e33b606 --- /dev/null +++ b/oms_web/oms_web_new/src/components/MainLayout/AppSider.jsx @@ -0,0 +1,172 @@ +import { useState, useEffect } from 'react' +import { Layout, Menu, Badge, Tooltip } from 'antd' +import { useNavigate, useLocation } from 'react-router-dom' +import { + DashboardOutlined, + GlobalOutlined, + CloudServerOutlined, + UserOutlined, + AppstoreOutlined, + SettingOutlined, +} from '@ant-design/icons' +import menuData from '../../constants/menuData.json' +import './AppSider.css' + +const { Sider } = Layout +const { SubMenu } = Menu + +// 图标映射 +const iconMap = { + DashboardOutlined: DashboardOutlined, + GlobalOutlined: GlobalOutlined, + CloudServerOutlined: CloudServerOutlined, + UserOutlined: UserOutlined, + AppstoreOutlined: AppstoreOutlined, + SettingOutlined: SettingOutlined, +} + +function AppSider({ collapsed, onToggle }) { + const navigate = useNavigate() + const location = useLocation() + const [openKeys, setOpenKeys] = useState([]) + + // 根据当前路径获取应该打开的父菜单 + const getDefaultOpenKeys = () => { + const path = location.pathname + for (const item of menuData) { + if (item.children) { + const hasChild = item.children.some((c) => c.path === path) + if (hasChild) { + return [item.key] + } + } + } + return [] + } + + // 监听路径变化和收拢状态,自动打开父菜单 + useEffect(() => { + if (!collapsed) { + const defaultKeys = getDefaultOpenKeys() + setOpenKeys(defaultKeys) + } + }, [location.pathname, collapsed]) + + const handleMenuClick = ({ key }) => { + // 查找对应的路径 + for (const item of menuData) { + if (item.key === key && item.path) { + navigate(item.path) + return + } + if (item.children) { + const child = item.children.find((c) => c.key === key) + if (child) { + navigate(child.path) + return + } + } + } + } + + const handleOpenChange = (keys) => { + setOpenKeys(keys) + } + + // 获取当前选中的菜单项 + const getSelectedKey = () => { + const path = location.pathname + for (const item of menuData) { + if (item.path === path) return item.key + if (item.children) { + const child = item.children.find((c) => c.path === path) + if (child) return child.key + } + } + return 'overview' + } + + // 渲染菜单项 + const renderMenuItems = () => { + return menuData.map((item) => { + const IconComponent = iconMap[item.icon] + const icon = IconComponent ? : null + + // 如果有子菜单 + if (item.children) { + // 在收拢状态下,用 Tooltip 包装 SubMenu 的图标 + const subMenuIcon = collapsed ? ( + + {icon} + + ) : ( + icon + ) + + return ( + + {item.children.map((child) => ( + + {child.badge ? ( + + {child.label} + + + ) : ( + child.label + )} + + ))} + + ) + } + + // 普通菜单项 - 也用 Tooltip 包装 + const menuIcon = collapsed ? ( + + {icon} + + ) : ( + icon + ) + + return ( + + {item.label} + + ) + }) + } + + return ( + + {/* 菜单 */} + + {renderMenuItems()} + + + ) +} + +export default AppSider diff --git a/oms_web/oms_web_new/src/components/MainLayout/MainLayout.css b/oms_web/oms_web_new/src/components/MainLayout/MainLayout.css new file mode 100644 index 00000000..386f3ff6 --- /dev/null +++ b/oms_web/oms_web_new/src/components/MainLayout/MainLayout.css @@ -0,0 +1,24 @@ +.main-layout { + min-height: 100vh; + display: flex; + flex-direction: column; + background: #fafafa; +} + +.main-content-wrapper { + display: flex; + flex: 1; + height: calc(100vh - 64px); + background: #fafafa; +} + +.main-content { + background: #f5f5f5; + overflow-y: auto; + flex: 1; +} + +.content-wrapper { + padding: 24px; + min-height: 100%; +} diff --git a/oms_web/oms_web_new/src/components/MainLayout/MainLayout.jsx b/oms_web/oms_web_new/src/components/MainLayout/MainLayout.jsx new file mode 100644 index 00000000..2c4ea0b2 --- /dev/null +++ b/oms_web/oms_web_new/src/components/MainLayout/MainLayout.jsx @@ -0,0 +1,31 @@ +import { useState } from 'react' +import { Layout } from 'antd' +import AppSider from './AppSider' +import AppHeader from './AppHeader' +import './MainLayout.css' + +const { Content } = Layout + +function MainLayout({ children }) { + const [collapsed, setCollapsed] = useState(false) + + const toggleCollapsed = () => { + setCollapsed(!collapsed) + } + + return ( + + + + + +
+ {children} +
+
+
+
+ ) +} + +export default MainLayout diff --git a/oms_web/oms_web_new/src/components/MainLayout/index.js b/oms_web/oms_web_new/src/components/MainLayout/index.js new file mode 100644 index 00000000..12555fdd --- /dev/null +++ b/oms_web/oms_web_new/src/components/MainLayout/index.js @@ -0,0 +1,4 @@ +export { default } from './MainLayout' +export { default as MainLayout } from './MainLayout' +export { default as AppSider } from './AppSider' +export { default as AppHeader } from './AppHeader' diff --git a/oms_web/oms_web_new/src/components/PageHeader/PageHeader.css b/oms_web/oms_web_new/src/components/PageHeader/PageHeader.css new file mode 100644 index 00000000..6ba9202e --- /dev/null +++ b/oms_web/oms_web_new/src/components/PageHeader/PageHeader.css @@ -0,0 +1,109 @@ +.page-header-standard { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + padding: 24px 28px; + margin-bottom: 24px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); + position: relative; + overflow: hidden; +} + +.page-header-standard::before { + content: ''; + position: absolute; + top: -50%; + right: -10%; + width: 300px; + height: 300px; + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; +} + +.page-header-main { + display: flex; + align-items: center; + gap: 16px; + position: relative; + z-index: 1; +} + +.back-button { + width: 36px; + height: 36px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s; + font-size: 16px; +} + +.back-button:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateX(-2px); +} + +.page-header-content { + display: flex; + align-items: center; + gap: 16px; +} + +.page-header-icon { + width: 48px; + height: 48px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + color: #ffffff; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.page-header-text { + display: flex; + flex-direction: column; + gap: 4px; +} + +.page-header-title { + font-size: 22px; + font-weight: 600; + color: #ffffff; + margin: 0; + letter-spacing: 0.3px; +} + +.page-header-description { + font-size: 14px; + color: rgba(255, 255, 255, 0.9); + margin: 0; + line-height: 1.5; +} + +.page-header-extra { + position: relative; + z-index: 1; +} + +@media (max-width: 768px) { + .page-header-standard { + flex-direction: column; + align-items: flex-start; + gap: 16px; + } + + .page-header-extra { + width: 100%; + } +} diff --git a/oms_web/oms_web_new/src/components/PageHeader/PageHeader.jsx b/oms_web/oms_web_new/src/components/PageHeader/PageHeader.jsx new file mode 100644 index 00000000..b934376d --- /dev/null +++ b/oms_web/oms_web_new/src/components/PageHeader/PageHeader.jsx @@ -0,0 +1,35 @@ +import { ArrowLeftOutlined } from '@ant-design/icons' +import './PageHeader.css' + +function PageHeader({ + title, + description, + icon, + showBack = false, + onBack, + extra +}) { + return ( +
+
+ {showBack && ( + + )} +
+ {icon &&
{icon}
} +
+

{title}

+ {description && ( +

{description}

+ )} +
+
+
+ {extra &&
{extra}
} +
+ ) +} + +export default PageHeader diff --git a/oms_web/oms_web_new/src/components/PageTitleBar/PageTitleBar.css b/oms_web/oms_web_new/src/components/PageTitleBar/PageTitleBar.css new file mode 100644 index 00000000..9913e326 --- /dev/null +++ b/oms_web/oms_web_new/src/components/PageTitleBar/PageTitleBar.css @@ -0,0 +1,165 @@ +.page-title-bar { + background: linear-gradient(135deg, #e0e7ff 0%, #f3e8ff 100%); + border-radius: 12px; + padding: 16px 24px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + position: relative; + overflow: hidden; + border: 1px solid rgba(139, 92, 246, 0.1); +} + +.page-title-bar::before { + content: ''; + position: absolute; + top: -50%; + right: -5%; + width: 200px; + height: 200px; + background: rgba(139, 92, 246, 0.05); + border-radius: 50%; +} + +.title-bar-content { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + align-items: center; +} + +.title-bar-left { + flex: 1; +} + +.title-row { + display: flex; + align-items: center; + gap: 16px; +} + +.title-group { + display: flex; + align-items: center; + gap: 12px; +} + +.page-title { + font-size: 20px; + font-weight: 600; + color: #1e293b; + margin: 0; + letter-spacing: 0.3px; +} + +.title-badge { + background: rgba(139, 92, 246, 0.15); + color: #7c3aed; + padding: 2px 10px; + border-radius: 10px; + font-size: 12px; + font-weight: 500; +} + +.page-description { + font-size: 13px; + color: #64748b; + margin: 0; + white-space: nowrap; +} + +.title-bar-right { + display: flex; + align-items: center; + gap: 12px; +} + +.title-actions { + display: flex; + gap: 10px; +} + +.title-actions button { + padding: 8px 16px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + border: none; + outline: none; +} + +.title-actions button.primary { + background: #7c3aed; + color: #ffffff; +} + +.title-actions button.primary:hover { + background: #6d28d9; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(124, 58, 237, 0.25); +} + +.title-actions button.secondary { + background: rgba(139, 92, 246, 0.1); + color: #7c3aed; + border: 1px solid rgba(139, 92, 246, 0.2); +} + +.title-actions button.secondary:hover { + background: rgba(139, 92, 246, 0.15); + transform: translateY(-1px); +} + +.toggle-button { + width: 32px; + height: 32px; + border-radius: 6px; + background: rgba(139, 92, 246, 0.1); + border: 1px solid rgba(139, 92, 246, 0.2); + color: #7c3aed; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s; + font-size: 14px; +} + +.toggle-button:hover { + background: rgba(139, 92, 246, 0.2); + transform: translateY(-1px); +} + +/* 响应式适配 */ +@media (max-width: 768px) { + .title-bar-content { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .title-row { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + + .page-description { + white-space: normal; + } + + .title-bar-right { + width: 100%; + justify-content: space-between; + } + + .title-actions { + flex: 1; + } + + .title-actions button { + flex: 1; + } +} diff --git a/oms_web/oms_web_new/src/components/PageTitleBar/PageTitleBar.jsx b/oms_web/oms_web_new/src/components/PageTitleBar/PageTitleBar.jsx new file mode 100644 index 00000000..870b861c --- /dev/null +++ b/oms_web/oms_web_new/src/components/PageTitleBar/PageTitleBar.jsx @@ -0,0 +1,53 @@ +import { useState } from 'react' +import { UpOutlined, DownOutlined } from '@ant-design/icons' +import './PageTitleBar.css' + +function PageTitleBar({ + title, + badge, + description, + actions, + showToggle = false, + onToggle, + defaultExpanded = true, +}) { + const [expanded, setExpanded] = useState(defaultExpanded) + + const handleToggle = () => { + const newExpanded = !expanded + setExpanded(newExpanded) + if (onToggle) { + onToggle(newExpanded) + } + } + + return ( +
+
+
+
+
+

{title}

+ {badge && {badge}} +
+ {description &&

{description}

} +
+
+
+ {actions &&
{actions}
} + {showToggle && ( + + )} +
+
+
+ ) +} + +export default PageTitleBar diff --git a/oms_web/oms_web_new/src/components/Toast/Toast.jsx b/oms_web/oms_web_new/src/components/Toast/Toast.jsx new file mode 100644 index 00000000..94862e33 --- /dev/null +++ b/oms_web/oms_web_new/src/components/Toast/Toast.jsx @@ -0,0 +1,114 @@ +import { notification } from 'antd' +import { + CheckCircleOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + InfoCircleOutlined, +} from '@ant-design/icons' + +// 配置全局通知位置和样式 +notification.config({ + placement: 'topRight', + top: 24, + duration: 3, + maxCount: 3, +}) + +/** + * 标准通知反馈组件 + * 从右上角滑出,默认3秒后消失 + */ +const Toast = { + /** + * 成功通知 + * @param {string} message - 消息内容 + * @param {string} description - 详细描述(可选) + * @param {number} duration - 显示时长(秒),默认3秒 + */ + success: (message, description = '', duration = 3) => { + notification.success({ + message, + description, + duration, + icon: , + style: { + borderRadius: '8px', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + }, + }) + }, + + /** + * 错误通知 + * @param {string} message - 消息内容 + * @param {string} description - 详细描述(可选) + * @param {number} duration - 显示时长(秒),默认3秒 + */ + error: (message, description = '', duration = 3) => { + notification.error({ + message, + description, + duration, + icon: , + style: { + borderRadius: '8px', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + }, + }) + }, + + /** + * 警告通知 + * @param {string} message - 消息内容 + * @param {string} description - 详细描述(可选) + * @param {number} duration - 显示时长(秒),默认3秒 + */ + warning: (message, description = '', duration = 3) => { + notification.warning({ + message, + description, + duration, + icon: , + style: { + borderRadius: '8px', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + }, + }) + }, + + /** + * 信息通知 + * @param {string} message - 消息内容 + * @param {string} description - 详细描述(可选) + * @param {number} duration - 显示时长(秒),默认3秒 + */ + info: (message, description = '', duration = 3) => { + notification.info({ + message, + description, + duration, + icon: , + style: { + borderRadius: '8px', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + }, + }) + }, + + /** + * 自定义通知 + * @param {Object} config - 完整的通知配置对象 + */ + custom: (config) => { + notification.open({ + ...config, + style: { + borderRadius: '8px', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + ...config.style, + }, + }) + }, +} + +export default Toast diff --git a/oms_web/oms_web_new/src/components/TreeFilterPanel/TreeFilterPanel.css b/oms_web/oms_web_new/src/components/TreeFilterPanel/TreeFilterPanel.css new file mode 100644 index 00000000..fd6341ce --- /dev/null +++ b/oms_web/oms_web_new/src/components/TreeFilterPanel/TreeFilterPanel.css @@ -0,0 +1,58 @@ +/* 树形筛选面板 */ +.tree-filter-panel { + width: 320px; + max-height: 500px; + overflow-y: auto; +} + +/* 已选择的筛选条件 */ +.tree-filter-selected { + min-height: 40px; + padding: 12px; + background: #f5f7fa; + border-radius: 6px; + border: 1px dashed #d9d9d9; +} + +.tree-filter-tag { + display: flex; + align-items: center; + gap: 8px; +} + +.tree-filter-label { + font-size: 13px; + color: rgba(0, 0, 0, 0.65); + font-weight: 500; +} + +.tree-filter-placeholder { + display: flex; + align-items: center; + justify-content: center; + min-height: 24px; +} + +.tree-filter-placeholder span { + color: #8c8c8c; + font-size: 13px; +} + +/* 树形选择器容器 */ +.tree-filter-container { + max-height: 280px; + overflow-y: auto; +} + +.tree-filter-header { + font-size: 14px; + font-weight: 500; + margin-bottom: 12px; + color: rgba(0, 0, 0, 0.85); +} + +/* 操作按钮 */ +.tree-filter-actions { + display: flex; + justify-content: flex-end; +} diff --git a/oms_web/oms_web_new/src/components/TreeFilterPanel/TreeFilterPanel.jsx b/oms_web/oms_web_new/src/components/TreeFilterPanel/TreeFilterPanel.jsx new file mode 100644 index 00000000..2723c226 --- /dev/null +++ b/oms_web/oms_web_new/src/components/TreeFilterPanel/TreeFilterPanel.jsx @@ -0,0 +1,119 @@ +import { Tree, Tag, Divider, Button, Space } from 'antd' +import { useState, useEffect } from 'react' +import './TreeFilterPanel.css' + +/** + * 树形筛选面板组件 + * @param {Object} props + * @param {Array} props.treeData - 树形数据 + * @param {string} props.selectedKey - 当前选中的节点ID + * @param {string} props.tempSelectedKey - 临时选中的节点ID(确认前) + * @param {string} props.treeTitle - 树标题 + * @param {Function} props.onSelect - 选择变化回调 + * @param {Function} props.onConfirm - 确认筛选 + * @param {Function} props.onClear - 清除筛选 + * @param {string} props.placeholder - 占位提示文本 + */ +function TreeFilterPanel({ + treeData, + selectedKey, + tempSelectedKey, + treeTitle = '分组筛选', + onSelect, + onConfirm, + onClear, + placeholder = '请选择分组进行筛选', +}) { + // 获取所有节点的key用于默认展开 + const getAllKeys = (nodes) => { + let keys = [] + const traverse = (node) => { + keys.push(node.key) + if (node.children) { + node.children.forEach(traverse) + } + } + nodes.forEach(traverse) + return keys + } + + const [expandedKeys, setExpandedKeys] = useState([]) + + // 初始化时展开所有节点 + useEffect(() => { + if (treeData && treeData.length > 0) { + setExpandedKeys(getAllKeys(treeData)) + } + }, [treeData]) + + // 查找节点名称 + const findNodeName = (nodes, id) => { + for (const node of nodes) { + if (node.key === id) return node.title + if (node.children) { + const found = findNodeName(node.children, id) + if (found) return found + } + } + return '' + } + + const handleTreeSelect = (selectedKeys) => { + const key = selectedKeys[0] || null + onSelect?.(key) + } + + const handleExpand = (keys) => { + setExpandedKeys(keys) + } + + return ( +
+ {/* 已选择的筛选条件 */} +
+ {tempSelectedKey ? ( +
+ 已选择分组: + onSelect?.(null)}> + {findNodeName(treeData, tempSelectedKey)} + +
+ ) : ( +
+ {placeholder} +
+ )} +
+ + + + {/* 树形选择器 */} +
+
{treeTitle}
+ +
+ + + + {/* 操作按钮 */} +
+ + + + +
+
+ ) +} + +export default TreeFilterPanel