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