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 (
+
+
+ {/* 顶部标题栏 - 固定不滚动 */}
+
+
+
}
+ onClick={onClose}
+ className="detail-drawer-close-button"
+ />
+
+ {title?.icon && {title.icon}}
+
{title?.text}
+ {title?.badge && {title.badge}}
+
+
+
+
+ {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"
+ >
+
}
+ type={filter.isActive ? 'primary' : 'default'}
+ >
+ {filter.selectedLabel || '筛选'}
+
+
+ )}
+
+ {showRefresh && (
+
} onClick={onRefresh}>
+ 刷新
+
+ )}
+
+
+ )
+}
+
+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 (
+
+ )
+}
+
+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 (
+
+ {/* 菜单 */}
+
+
+ )
+}
+
+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}
+
+ )}
+
+
+
+
+ {/* 树形选择器 */}
+
+
+
+
+ {/* 操作按钮 */}
+
+
+
+
+
+
+
+ )
+}
+
+export default TreeFilterPanel