refactor(components): 移除旧版布局和组件实现
- 删除 AppHeader 组件及其 CSS 样式文件 - 删除 AppSider 侧边栏组件及对应样式 - 移除 ConfirmDialog 确认对话框组件 - 删除 DetailDrawer 详情抽屉组件及相关样式 - 移除 InfoPanel 信息展示面板组件 - 删除 ListActionBar 列表操作栏组件 - 移除 ListTable 列表表格组件及样式 - 删除 MainLayout 布局组件及相关样式文件 - 清理组件导出入口文件 index.js 中的相关引用dev_1.0.0
parent
54c1e03830
commit
bd3a443443
|
|
@ -1,138 +0,0 @@
|
||||||
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: (
|
|
||||||
<div>
|
|
||||||
<p>您确定要删除以下项目吗?</p>
|
|
||||||
<div style={{ marginTop: 12, padding: 12, background: '#f5f5f5', borderRadius: 6 }}>
|
|
||||||
<p style={{ margin: 0, fontWeight: 500 }}>{itemName}</p>
|
|
||||||
{itemInfo && (
|
|
||||||
<p style={{ margin: '4px 0 0 0', fontSize: 13, color: '#666' }}>{itemInfo}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p style={{ marginTop: 12, color: '#ff4d4f', fontSize: 13 }}>
|
|
||||||
此操作不可恢复,请谨慎操作!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
okText: '确认删除',
|
|
||||||
cancelText: '取消',
|
|
||||||
okType: 'danger',
|
|
||||||
centered: true,
|
|
||||||
icon: <DeleteOutlined style={{ color: '#ff4d4f' }} />,
|
|
||||||
onOk,
|
|
||||||
onCancel,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示批量删除确认对话框
|
|
||||||
*/
|
|
||||||
batchDelete: ({ count, items, onOk, onCancel }) => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '批量删除确认',
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<p>您确定要删除选中的 {count} 个项目吗?</p>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: 12,
|
|
||||||
padding: 12,
|
|
||||||
background: '#f5f5f5',
|
|
||||||
borderRadius: 6,
|
|
||||||
maxHeight: 200,
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
padding: '6px 0',
|
|
||||||
borderBottom: index < items.length - 1 ? '1px solid #e8e8e8' : 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontWeight: 500 }}>{item.name}</span>
|
|
||||||
{item.info && (
|
|
||||||
<span style={{ marginLeft: 12, fontSize: 13, color: '#666' }}>
|
|
||||||
({item.info})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p style={{ marginTop: 12, color: '#ff4d4f', fontSize: 13 }}>
|
|
||||||
此操作不可恢复,请谨慎操作!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
okText: '确认删除',
|
|
||||||
cancelText: '取消',
|
|
||||||
okType: 'danger',
|
|
||||||
centered: true,
|
|
||||||
icon: <DeleteOutlined style={{ color: '#ff4d4f' }} />,
|
|
||||||
onOk,
|
|
||||||
onCancel,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示警告确认对话框
|
|
||||||
*/
|
|
||||||
warning: ({ title, content, okText = '确定', cancelText = '取消', onOk, onCancel }) => {
|
|
||||||
Modal.confirm({
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
okText,
|
|
||||||
cancelText,
|
|
||||||
centered: true,
|
|
||||||
icon: <ExclamationCircleOutlined style={{ color: '#faad14' }} />,
|
|
||||||
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
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
/* 详情抽屉容器 */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
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 (
|
|
||||||
<Drawer
|
|
||||||
title={null}
|
|
||||||
placement="right"
|
|
||||||
width={width}
|
|
||||||
onClose={onClose}
|
|
||||||
open={visible}
|
|
||||||
closable={false}
|
|
||||||
styles={{ body: { padding: 0 } }}
|
|
||||||
>
|
|
||||||
<div className="detail-drawer-content">
|
|
||||||
{/* 顶部标题栏 - 固定不滚动 */}
|
|
||||||
<div className="detail-drawer-header">
|
|
||||||
<div className="detail-drawer-header-left">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={onClose}
|
|
||||||
className="detail-drawer-close-button"
|
|
||||||
/>
|
|
||||||
<div className="detail-drawer-header-info">
|
|
||||||
{title?.icon && <span className="detail-drawer-title-icon">{title.icon}</span>}
|
|
||||||
<h2 className="detail-drawer-title">{title?.text}</h2>
|
|
||||||
{title?.badge && <span className="detail-drawer-badge">{title.badge}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="detail-drawer-header-right">
|
|
||||||
<Space size="middle">
|
|
||||||
{headerActions.map((action) => (
|
|
||||||
<Button
|
|
||||||
key={action.key}
|
|
||||||
type={action.type || 'default'}
|
|
||||||
icon={action.icon}
|
|
||||||
danger={action.danger}
|
|
||||||
disabled={action.disabled}
|
|
||||||
onClick={action.onClick}
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 可滚动内容区域 */}
|
|
||||||
<div className="detail-drawer-scrollable-content">
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{/* 可选的标签页区域 */}
|
|
||||||
{tabs && tabs.length > 0 && (
|
|
||||||
<div className="detail-drawer-tabs">
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey={tabs[0].key}
|
|
||||||
type="line"
|
|
||||||
size="large"
|
|
||||||
items={tabs.map((tab) => ({
|
|
||||||
key: tab.key,
|
|
||||||
label: tab.label,
|
|
||||||
children: <div className="detail-drawer-tab-content">{tab.content}</div>,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DetailDrawer
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
/* 信息面板 */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
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 (
|
|
||||||
<div className="info-panel">
|
|
||||||
<Row gutter={gutter}>
|
|
||||||
{fields.map((field) => {
|
|
||||||
const value = data[field.key]
|
|
||||||
const displayValue = field.render ? field.render(value, data) : value
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col key={field.key} span={field.span || 6}>
|
|
||||||
<div className="info-panel-item">
|
|
||||||
<div className="info-panel-label">{field.label}</div>
|
|
||||||
<div className="info-panel-value">{displayValue}</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{/* 可选的操作按钮区 */}
|
|
||||||
{actions && actions.length > 0 && (
|
|
||||||
<div className="info-panel-actions">
|
|
||||||
<Space size="middle">
|
|
||||||
{actions.map((action) => (
|
|
||||||
<Button
|
|
||||||
key={action.key}
|
|
||||||
type={action.type || 'default'}
|
|
||||||
icon={action.icon}
|
|
||||||
disabled={action.disabled}
|
|
||||||
danger={action.danger}
|
|
||||||
onClick={action.onClick}
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InfoPanel
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
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 (
|
|
||||||
<div className="list-action-bar">
|
|
||||||
{/* 左侧操作按钮区 */}
|
|
||||||
<div className="list-action-bar-left">
|
|
||||||
{actions.map((action) => (
|
|
||||||
<Button
|
|
||||||
key={action.key}
|
|
||||||
type={action.type || 'default'}
|
|
||||||
icon={action.icon}
|
|
||||||
disabled={action.disabled}
|
|
||||||
danger={action.danger}
|
|
||||||
onClick={action.onClick}
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧搜索筛选区 */}
|
|
||||||
<div className="list-action-bar-right">
|
|
||||||
<Space.Compact>
|
|
||||||
<Search
|
|
||||||
placeholder={search?.placeholder || '请输入搜索关键词'}
|
|
||||||
allowClear
|
|
||||||
style={{ width: search?.width || 280 }}
|
|
||||||
onSearch={search?.onSearch}
|
|
||||||
onChange={(e) => search?.onChange?.(e.target.value)}
|
|
||||||
value={search?.value}
|
|
||||||
/>
|
|
||||||
{filter && (
|
|
||||||
<Popover
|
|
||||||
content={filter.content}
|
|
||||||
title={
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<FilterOutlined />
|
|
||||||
<span>{filter.title || '高级筛选'}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
trigger="click"
|
|
||||||
open={filter.visible}
|
|
||||||
onOpenChange={filter.onVisibleChange}
|
|
||||||
placement="bottomRight"
|
|
||||||
overlayClassName="filter-popover"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={<FilterOutlined />}
|
|
||||||
type={filter.isActive ? 'primary' : 'default'}
|
|
||||||
>
|
|
||||||
{filter.selectedLabel || '筛选'}
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
</Space.Compact>
|
|
||||||
{showRefresh && (
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={onRefresh}>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListActionBar
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
/* 列表表格容器 */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
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 (
|
|
||||||
<div className={`list-table-container ${className}`}>
|
|
||||||
<Table
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
columns={columns}
|
|
||||||
dataSource={dataSource}
|
|
||||||
rowKey={rowKey}
|
|
||||||
pagination={{
|
|
||||||
...pagination,
|
|
||||||
total: dataSource?.length || 0,
|
|
||||||
}}
|
|
||||||
scroll={scroll}
|
|
||||||
loading={loading}
|
|
||||||
onRow={(record) => ({
|
|
||||||
onClick: () => onRowClick?.(record),
|
|
||||||
className: selectedRow?.[rowKey] === record[rowKey] ? 'row-selected' : '',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListTable
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
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: <QuestionCircleOutlined />,
|
|
||||||
FileTextOutlined: <FileTextOutlined />,
|
|
||||||
CustomerServiceOutlined: <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 (
|
|
||||||
<Header className="app-header">
|
|
||||||
{/* 左侧:Logo + 折叠按钮 */}
|
|
||||||
<div className="header-left">
|
|
||||||
{/* Logo 区域 */}
|
|
||||||
<div className="header-logo">
|
|
||||||
<img src={logoFull} alt="NEX Console" className="logo-full" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 折叠按钮 */}
|
|
||||||
<div className="trigger" onClick={onToggle}>
|
|
||||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧:搜索 + 功能按钮 + 用户信息 */}
|
|
||||||
<div className="header-right">
|
|
||||||
{/* 搜索框 */}
|
|
||||||
<Input
|
|
||||||
className="header-search"
|
|
||||||
placeholder="搜索..."
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
style={{ width: 200 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 功能图标 */}
|
|
||||||
<Space size={16} className="header-actions">
|
|
||||||
{/* 动态渲染 header 菜单 */}
|
|
||||||
{headerMenuData.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.key}
|
|
||||||
className="header-link"
|
|
||||||
title={item.label}
|
|
||||||
onClick={() => handleHeaderMenuClick(item.key)}
|
|
||||||
>
|
|
||||||
{iconMap[item.icon]}
|
|
||||||
<span className="ml-1">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 消息中心 */}
|
|
||||||
<Badge count={5} size="small" offset={[-3, 3]}>
|
|
||||||
<div className="header-icon" title="消息中心">
|
|
||||||
<BellOutlined />
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
{/* 用户下拉菜单 */}
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items: userMenuItems,
|
|
||||||
onClick: handleUserMenuClick,
|
|
||||||
}}
|
|
||||||
placement="bottomRight"
|
|
||||||
>
|
|
||||||
<div className="user-info">
|
|
||||||
<Avatar size={32} icon={<UserOutlined />} />
|
|
||||||
<span className="username">Admin</span>
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppHeader
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
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 ? <IconComponent /> : null
|
|
||||||
|
|
||||||
// 如果有子菜单
|
|
||||||
if (item.children) {
|
|
||||||
// 在收拢状态下,用 Tooltip 包装 SubMenu 的图标
|
|
||||||
const subMenuIcon = collapsed ? (
|
|
||||||
<Tooltip title={item.label} placement="right">
|
|
||||||
{icon}
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
icon
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SubMenu
|
|
||||||
key={item.key}
|
|
||||||
icon={subMenuIcon}
|
|
||||||
title={item.label}
|
|
||||||
popupClassName="sider-submenu-popup"
|
|
||||||
>
|
|
||||||
{item.children.map((child) => (
|
|
||||||
<Menu.Item key={child.key}>
|
|
||||||
{child.badge ? (
|
|
||||||
<span className="menu-item-with-badge">
|
|
||||||
{child.label}
|
|
||||||
<Badge
|
|
||||||
count={child.badge}
|
|
||||||
className={`menu-badge ${child.badge === 'HOT' ? 'badge-hot' : 'badge-new'}`}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
child.label
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</SubMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通菜单项 - 也用 Tooltip 包装
|
|
||||||
const menuIcon = collapsed ? (
|
|
||||||
<Tooltip title={item.label} placement="right">
|
|
||||||
{icon}
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
icon
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu.Item key={item.key} icon={menuIcon} title="">
|
|
||||||
{item.label}
|
|
||||||
</Menu.Item>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sider
|
|
||||||
className="app-sider"
|
|
||||||
collapsed={collapsed}
|
|
||||||
width={200}
|
|
||||||
collapsedWidth={64}
|
|
||||||
trigger={null}
|
|
||||||
>
|
|
||||||
{/* 菜单 */}
|
|
||||||
<Menu
|
|
||||||
mode="inline"
|
|
||||||
selectedKeys={[getSelectedKey()]}
|
|
||||||
openKeys={collapsed ? [] : openKeys}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
onClick={handleMenuClick}
|
|
||||||
className="sider-menu"
|
|
||||||
>
|
|
||||||
{renderMenuItems()}
|
|
||||||
</Menu>
|
|
||||||
</Sider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppSider
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
.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%;
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
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 (
|
|
||||||
<Layout className="main-layout">
|
|
||||||
<AppHeader collapsed={collapsed} onToggle={toggleCollapsed} />
|
|
||||||
<Layout className="main-content-wrapper">
|
|
||||||
<AppSider collapsed={collapsed} onToggle={toggleCollapsed} />
|
|
||||||
<Content className="main-content">
|
|
||||||
<div className="content-wrapper">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MainLayout
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export { default } from './MainLayout'
|
|
||||||
export { default as MainLayout } from './MainLayout'
|
|
||||||
export { default as AppSider } from './AppSider'
|
|
||||||
export { default as AppHeader } from './AppHeader'
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
.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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
|
||||||
import './PageHeader.css'
|
|
||||||
|
|
||||||
function PageHeader({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
showBack = false,
|
|
||||||
onBack,
|
|
||||||
extra
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="page-header-standard">
|
|
||||||
<div className="page-header-main">
|
|
||||||
{showBack && (
|
|
||||||
<button className="back-button" onClick={onBack}>
|
|
||||||
<ArrowLeftOutlined />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="page-header-content">
|
|
||||||
{icon && <div className="page-header-icon">{icon}</div>}
|
|
||||||
<div className="page-header-text">
|
|
||||||
<h1 className="page-header-title">{title}</h1>
|
|
||||||
{description && (
|
|
||||||
<p className="page-header-description">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{extra && <div className="page-header-extra">{extra}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageHeader
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
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 (
|
|
||||||
<div className="page-title-bar">
|
|
||||||
<div className="title-bar-content">
|
|
||||||
<div className="title-bar-left">
|
|
||||||
<div className="title-row">
|
|
||||||
<div className="title-group">
|
|
||||||
<h1 className="page-title">{title}</h1>
|
|
||||||
{badge && <span className="title-badge">{badge}</span>}
|
|
||||||
</div>
|
|
||||||
{description && <p className="page-description">{description}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="title-bar-right">
|
|
||||||
{actions && <div className="title-actions">{actions}</div>}
|
|
||||||
{showToggle && (
|
|
||||||
<button
|
|
||||||
className="toggle-button"
|
|
||||||
onClick={handleToggle}
|
|
||||||
title={expanded ? '收起信息面板' : '展开信息面板'}
|
|
||||||
>
|
|
||||||
{expanded ? <UpOutlined /> : <DownOutlined />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageTitleBar
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
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: <CheckCircleOutlined style={{ color: '#52c41a' }} />,
|
|
||||||
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: <CloseCircleOutlined style={{ color: '#ff4d4f' }} />,
|
|
||||||
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: <ExclamationCircleOutlined style={{ color: '#faad14' }} />,
|
|
||||||
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: <InfoCircleOutlined style={{ color: '#1677ff' }} />,
|
|
||||||
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
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
/* 树形筛选面板 */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
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 (
|
|
||||||
<div className="tree-filter-panel">
|
|
||||||
{/* 已选择的筛选条件 */}
|
|
||||||
<div className="tree-filter-selected">
|
|
||||||
{tempSelectedKey ? (
|
|
||||||
<div className="tree-filter-tag">
|
|
||||||
<span className="tree-filter-label">已选择分组:</span>
|
|
||||||
<Tag color="blue" closable onClose={() => onSelect?.(null)}>
|
|
||||||
{findNodeName(treeData, tempSelectedKey)}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="tree-filter-placeholder">
|
|
||||||
<span>{placeholder}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
|
||||||
|
|
||||||
{/* 树形选择器 */}
|
|
||||||
<div className="tree-filter-container">
|
|
||||||
<div className="tree-filter-header">{treeTitle}</div>
|
|
||||||
<Tree
|
|
||||||
treeData={treeData}
|
|
||||||
expandedKeys={expandedKeys}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
onSelect={handleTreeSelect}
|
|
||||||
selectedKeys={tempSelectedKey ? [tempSelectedKey] : []}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<div className="tree-filter-actions">
|
|
||||||
<Space>
|
|
||||||
<Button size="small" onClick={onClear}>
|
|
||||||
清除筛选
|
|
||||||
</Button>
|
|
||||||
<Button size="small" type="primary" onClick={onConfirm}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TreeFilterPanel
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package com.ruoyi.sip.controller.vue;
|
||||||
|
|
||||||
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.sip.domain.OmsStockInfo;
|
||||||
|
import com.ruoyi.sip.service.IOmsStockInfoService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ch
|
||||||
|
* @Desc 备货信息(OmsStockInfo)表Vue控制器
|
||||||
|
* @Date 2025-10-16 09:43:37
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/stock/vue")
|
||||||
|
public class VueStockController extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IOmsStockInfoService omsStockInfoService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询备货信息列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(OmsStockInfo omsStockInfo) {
|
||||||
|
startPage();
|
||||||
|
List<OmsStockInfo> list = omsStockInfoService.queryAll(omsStockInfo);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取备货信息详细信息
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/{id}")
|
||||||
|
public AjaxResult getInfo(@PathVariable("id") Integer id) {
|
||||||
|
return AjaxResult.success(omsStockInfoService.queryById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增备货信息
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public AjaxResult add(@RequestBody OmsStockInfo omsStockInfo) {
|
||||||
|
return toAjax(omsStockInfoService.insert(omsStockInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改备货信息
|
||||||
|
*/
|
||||||
|
@PutMapping
|
||||||
|
@Log(title = "备货管理", businessType = BusinessType.UPDATE)
|
||||||
|
public AjaxResult edit(@RequestBody OmsStockInfo omsStockInfo) {
|
||||||
|
return toAjax(omsStockInfoService.update(omsStockInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除备货信息
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{ids}")
|
||||||
|
@Log(title = "备货管理", businessType = BusinessType.DELETE)
|
||||||
|
public AjaxResult remove(@PathVariable String ids) {
|
||||||
|
return toAjax(omsStockInfoService.batchRemove(ids.split(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出备货信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/export")
|
||||||
|
@Log(title = "备货管理", businessType = BusinessType.EXPORT)
|
||||||
|
public void export(OmsStockInfo omsStockInfo, HttpServletResponse response) throws IOException {
|
||||||
|
// This is a placeholder for actual Excel export logic.
|
||||||
|
// In a real project, this would typically involve using a utility class like ExcelUtil
|
||||||
|
// to write the list of OmsStockInfo objects to an Excel file and send it as a response.
|
||||||
|
List<OmsStockInfo> list = omsStockInfoService.queryAll(omsStockInfo);
|
||||||
|
// Example:
|
||||||
|
// ExcelUtil<OmsStockInfo> util = new ExcelUtil<OmsStockInfo>(OmsStockInfo.class);
|
||||||
|
// util.exportExcel(response, list, "StockData");
|
||||||
|
System.out.println("Export functionality called for OmsStockInfo with " + list.size() + " items.");
|
||||||
|
// A minimal response to avoid client timeout/error if no actual file is written.
|
||||||
|
response.setContentType("text/plain");
|
||||||
|
response.getWriter().write("Export command received. Check server logs for details.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue