v 1.0.0
parent
c163adf178
commit
3665022575
|
|
@ -0,0 +1,330 @@
|
|||
# ConfirmDialog 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
确认对话框组件,基于 Ant Design Modal 封装,提供统一的确认对话框样式和交互。支持单个删除、批量删除、警告确认和通用确认等多种场景。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/ConfirmDialog/ConfirmDialog.jsx
|
||||
```
|
||||
|
||||
## API 方法
|
||||
|
||||
组件以静态方法的形式提供,无需实例化,直接调用即可。
|
||||
|
||||
### ConfirmDialog.delete()
|
||||
|
||||
显示单个项目删除确认对话框。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| title | string | 否 | '确认删除' | 对话框标题 |
|
||||
| itemName | string | 是 | - | 要删除的项目名称 |
|
||||
| itemInfo | string | 否 | - | 项目附加信息 |
|
||||
| onOk | function | 否 | - | 确认回调,支持返回 Promise |
|
||||
| onCancel | function | 否 | - | 取消回调 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||
|
||||
const handleDeleteUser = (record) => {
|
||||
ConfirmDialog.delete({
|
||||
itemName: `用户名:${record.userName}`,
|
||||
itemInfo: `姓名:${record.name}`,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
// 执行删除操作
|
||||
deleteUser(record.id)
|
||||
resolve()
|
||||
Toast.success('删除成功', `用户 "${record.userName}" 已成功删除`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### ConfirmDialog.batchDelete()
|
||||
|
||||
显示批量删除确认对话框。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| count | number | 是 | - | 要删除的项目数量 |
|
||||
| items | Array<ItemInfo> | 是 | - | 项目列表 |
|
||||
| onOk | function | 否 | - | 确认回调,支持返回 Promise |
|
||||
| onCancel | function | 否 | - | 取消回调 |
|
||||
|
||||
##### ItemInfo 对象
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| name | string | 是 | 项目名称 |
|
||||
| info | string | 否 | 项目附加信息 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
const handleBatchDelete = () => {
|
||||
const selectedUsers = filteredUsers.filter((u) => selectedRowKeys.includes(u.id))
|
||||
const items = selectedUsers.map((user) => ({
|
||||
name: user.userName,
|
||||
info: user.name,
|
||||
}))
|
||||
|
||||
ConfirmDialog.batchDelete({
|
||||
count: selectedRowKeys.length,
|
||||
items,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const count = selectedRowKeys.length
|
||||
const newUsers = filteredUsers.filter((u) => !selectedRowKeys.includes(u.id))
|
||||
setFilteredUsers(newUsers)
|
||||
setSelectedRowKeys([])
|
||||
resolve()
|
||||
Toast.success('批量删除成功', `已成功删除 ${count} 个用户`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### ConfirmDialog.warning()
|
||||
|
||||
显示警告确认对话框。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| title | string | 是 | - | 对话框标题 |
|
||||
| content | string\|ReactNode | 是 | - | 对话框内容 |
|
||||
| okText | string | 否 | '确定' | 确认按钮文字 |
|
||||
| cancelText | string | 否 | '取消' | 取消按钮文字 |
|
||||
| onOk | function | 否 | - | 确认回调 |
|
||||
| onCancel | function | 否 | - | 取消回调 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
const handleRiskyOperation = () => {
|
||||
ConfirmDialog.warning({
|
||||
title: '操作警告',
|
||||
content: '此操作可能影响系统稳定性,确定要继续吗?',
|
||||
okText: '继续操作',
|
||||
onOk() {
|
||||
// 执行危险操作
|
||||
performRiskyOperation()
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### ConfirmDialog.confirm()
|
||||
|
||||
显示通用确认对话框。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| title | string | 是 | - | 对话框标题 |
|
||||
| content | string\|ReactNode | 是 | - | 对话框内容 |
|
||||
| okText | string | 否 | '确定' | 确认按钮文字 |
|
||||
| cancelText | string | 否 | '取消' | 取消按钮文字 |
|
||||
| okType | string | 否 | 'primary' | 确认按钮类型(primary/danger) |
|
||||
| onOk | function | 否 | - | 确认回调 |
|
||||
| onCancel | function | 否 | - | 取消回调 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
const handleSubmit = () => {
|
||||
ConfirmDialog.confirm({
|
||||
title: '提交确认',
|
||||
content: '确定要提交当前表单吗?',
|
||||
okText: '提交',
|
||||
onOk() {
|
||||
// 执行提交操作
|
||||
submitForm()
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 完整使用示例
|
||||
|
||||
### 单个删除
|
||||
|
||||
```jsx
|
||||
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||
import Toast from '../components/Toast/Toast'
|
||||
|
||||
function UserListPage() {
|
||||
const handleDeleteUser = (record) => {
|
||||
ConfirmDialog.delete({
|
||||
itemName: `用户名:${record.userName}`,
|
||||
itemInfo: `姓名:${record.name}`,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newUsers = filteredUsers.filter((u) => u.id !== record.id)
|
||||
setFilteredUsers(newUsers)
|
||||
resolve()
|
||||
Toast.success('删除成功', `用户 "${record.userName}" 已成功删除`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={() => handleDeleteUser(user)} danger>
|
||||
删除
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 批量删除
|
||||
|
||||
```jsx
|
||||
function UserListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
const selectedUsers = filteredUsers.filter((u) => selectedRowKeys.includes(u.id))
|
||||
const items = selectedUsers.map((user) => ({
|
||||
name: user.userName,
|
||||
info: user.name,
|
||||
}))
|
||||
|
||||
ConfirmDialog.batchDelete({
|
||||
count: selectedRowKeys.length,
|
||||
items,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const count = selectedRowKeys.length
|
||||
const newUsers = filteredUsers.filter((u) => !selectedRowKeys.includes(u.id))
|
||||
setFilteredUsers(newUsers)
|
||||
setSelectedRowKeys([])
|
||||
resolve()
|
||||
Toast.success('批量删除成功', `已成功删除 ${count} 个用户`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
danger
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义内容
|
||||
|
||||
```jsx
|
||||
ConfirmDialog.confirm({
|
||||
title: '重置密码',
|
||||
content: (
|
||||
<div>
|
||||
<p>确定要重置用户 <strong>{user.userName}</strong> 的密码吗?</p>
|
||||
<p style={{ color: '#faad14' }}>新密码将发送到用户邮箱</p>
|
||||
</div>
|
||||
),
|
||||
okText: '确认重置',
|
||||
okType: 'primary',
|
||||
onOk() {
|
||||
resetPassword(user.id)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 异步操作处理
|
||||
|
||||
```jsx
|
||||
const handleDeleteUser = (record) => {
|
||||
ConfirmDialog.delete({
|
||||
itemName: `用户名:${record.userName}`,
|
||||
itemInfo: `姓名:${record.name}`,
|
||||
async onOk() {
|
||||
try {
|
||||
// 调用 API 删除用户
|
||||
await api.deleteUser(record.id)
|
||||
|
||||
// 更新本地数据
|
||||
const newUsers = filteredUsers.filter((u) => u.id !== record.id)
|
||||
setFilteredUsers(newUsers)
|
||||
|
||||
Toast.success('删除成功', `用户 "${record.userName}" 已成功删除`)
|
||||
} catch (error) {
|
||||
Toast.error('删除失败', error.message)
|
||||
throw error // 阻止对话框关闭
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 特性说明
|
||||
|
||||
### 删除确认样式
|
||||
|
||||
- 红色危险图标
|
||||
- 高亮显示要删除的项目信息
|
||||
- 红色警告提示"此操作不可恢复,请谨慎操作!"
|
||||
- 危险样式的确认按钮
|
||||
|
||||
### 批量删除列表
|
||||
|
||||
- 最多显示 200px 高度的滚动列表
|
||||
- 每个项目显示名称和附加信息
|
||||
- 项目间用分隔线隔开
|
||||
|
||||
### Promise 支持
|
||||
|
||||
- `onOk` 回调支持返回 Promise
|
||||
- 异步操作进行时,确认按钮显示 loading 状态
|
||||
- Promise reject 时,对话框不会关闭
|
||||
- 适合调用 API 等异步操作
|
||||
|
||||
### 居中显示
|
||||
|
||||
- 所有对话框默认垂直居中显示(`centered: true`)
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **删除确认** - 删除用户、设备、订单等数据前的确认
|
||||
2. **批量删除** - 批量删除多条数据前的确认
|
||||
3. **危险操作警告** - 执行可能影响系统的操作前的警告
|
||||
4. **通用确认** - 提交表单、保存设置等操作的确认
|
||||
5. **重要操作二次确认** - 任何需要用户明确确认的操作
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 删除操作统一使用红色危险样式,提醒用户谨慎操作
|
||||
2. `onOk` 回调支持同步和异步(返回 Promise)两种方式
|
||||
3. 批量删除时,`items` 数组会在对话框中完整展示,注意数量控制
|
||||
4. 对话框内容支持字符串和 React 节点,可以自定义复杂内容
|
||||
5. 确认按钮文字建议明确操作类型,如"确认删除"、"确认提交"等
|
||||
6. 配合 Toast 组件使用,提供操作结果反馈
|
||||
7. 异步操作失败时,throw error 可以阻止对话框关闭
|
||||
|
|
@ -0,0 +1,469 @@
|
|||
# DetailDrawer 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
详情抽屉组件,用于从页面右侧滑出显示详细信息。支持自定义标题、操作按钮、标签页等功能,内容区域可滚动,顶部标题栏固定。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/DetailDrawer/DetailDrawer.jsx
|
||||
src/components/DetailDrawer/DetailDrawer.css
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| visible | boolean | 是 | - | 是否显示抽屉 |
|
||||
| onClose | function | 是 | - | 关闭抽屉回调 |
|
||||
| title | TitleConfig | 否 | - | 标题配置对象 |
|
||||
| headerActions | Array<ActionConfig> | 否 | [] | 顶部操作按钮数组 |
|
||||
| width | number | 否 | 1080 | 抽屉宽度(像素) |
|
||||
| children | ReactNode | 否 | - | 主要内容区域 |
|
||||
| tabs | Array<TabConfig> | 否 | - | 标签页配置数组 |
|
||||
|
||||
### 1. 小型抽屉 (Small) - 480px
|
||||
**适用场景:**
|
||||
- 简单的信息展示
|
||||
- 少量字段的表单(1-3个字段)
|
||||
- 快速操作面板
|
||||
- 通知详情
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={480} ... />
|
||||
```
|
||||
|
||||
### 2. 中型抽屉 (Medium) - 720px
|
||||
**适用场景:**
|
||||
- 详细信息展示(如主机详情)
|
||||
- 中等复杂度的表单(4-10个字段)
|
||||
- 数据编辑面板
|
||||
- 配置设置
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={720} ... />
|
||||
```
|
||||
**当前主机列表页面使用此宽度模式**
|
||||
|
||||
### 3. 大型抽屉 (Large) - 1080px
|
||||
**适用场景:**
|
||||
- 复杂的多步骤表单
|
||||
- 需要并排展示多列信息
|
||||
- 包含图表或复杂可视化内容
|
||||
- 嵌套子表格或列表
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={1080} ... />
|
||||
```
|
||||
|
||||
### TitleConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| text | string | 是 | 标题文本 |
|
||||
| badge | ReactNode | 否 | 状态徽标(如 Tag、Badge 组件) |
|
||||
| icon | ReactNode | 否 | 标题图标 |
|
||||
|
||||
### ActionConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| key | string | 是 | 按钮唯一标识 |
|
||||
| label | string | 是 | 按钮文本 |
|
||||
| icon | ReactNode | 否 | 按钮图标 |
|
||||
| type | string | 否 | 按钮类型(primary/default/dashed/text/link) |
|
||||
| danger | boolean | 否 | 是否为危险按钮 |
|
||||
| disabled | boolean | 否 | 是否禁用 |
|
||||
| onClick | function | 否 | 点击回调函数 |
|
||||
|
||||
### TabConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| key | string | 是 | 标签页唯一标识 |
|
||||
| label | ReactNode | 是 | 标签页标题(支持图标+文字) |
|
||||
| content | ReactNode | 是 | 标签页内容 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||
|
||||
function MyPage() {
|
||||
const [showDrawer, setShowDrawer] = useState(false)
|
||||
const [selectedItem, setSelectedItem] = useState(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setShowDrawer(true)}>查看详情</Button>
|
||||
|
||||
<DetailDrawer
|
||||
visible={showDrawer}
|
||||
onClose={() => setShowDrawer(false)}
|
||||
title={{
|
||||
text: selectedItem?.name || '详情',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: 24 }}>
|
||||
<p>详情内容</p>
|
||||
</div>
|
||||
</DetailDrawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 带状态徽标
|
||||
|
||||
```jsx
|
||||
import { Tag, Badge } from 'antd'
|
||||
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
title={{
|
||||
text: selectedUser?.userName || '',
|
||||
badge: (
|
||||
<Tag color={selectedUser?.status === 'enabled' ? 'green' : 'default'}>
|
||||
{selectedUser?.status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{/* 内容 */}
|
||||
</DetailDrawer>
|
||||
```
|
||||
|
||||
### 带操作按钮
|
||||
|
||||
```jsx
|
||||
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
title={{
|
||||
text: selectedUser?.userName || '',
|
||||
}}
|
||||
headerActions={[
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setEditMode('edit')
|
||||
setShowEditDrawer(true)
|
||||
setShowDetailDrawer(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
setShowDetailDrawer(false)
|
||||
handleDelete(selectedUser)
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* 内容 */}
|
||||
</DetailDrawer>
|
||||
```
|
||||
|
||||
### 使用 InfoPanel 显示信息
|
||||
|
||||
```jsx
|
||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
||||
|
||||
const userFields = [
|
||||
{ key: 'userName', label: '用户名', span: 6 },
|
||||
{ key: 'group', label: '用户分组', span: 6 },
|
||||
{ key: 'name', label: '姓名', span: 6 },
|
||||
{ key: 'userType', label: '用户类型', span: 6 },
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
span: 6,
|
||||
render: (value) => (
|
||||
<Tag color={value === 'enabled' ? 'green' : 'default'}>
|
||||
{value === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
title={{
|
||||
text: selectedUser?.userName || '',
|
||||
}}
|
||||
>
|
||||
<InfoPanel
|
||||
data={selectedUser}
|
||||
fields={userFields}
|
||||
actions={[
|
||||
{ key: 'reset', label: '重置密码', onClick: () => console.log('重置密码') },
|
||||
{ key: 'disable', label: '停用', onClick: () => console.log('停用') },
|
||||
]}
|
||||
/>
|
||||
</DetailDrawer>
|
||||
```
|
||||
|
||||
### 带标签页
|
||||
|
||||
```jsx
|
||||
import { DatabaseOutlined, UserOutlined } from '@ant-design/icons'
|
||||
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
title={{
|
||||
text: selectedHost?.name || '',
|
||||
badge: (
|
||||
<Badge
|
||||
status={selectedHost?.status === 'online' ? 'success' : 'default'}
|
||||
text={selectedHost?.status === 'online' ? '在线' : '离线'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
headerActions={[
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
icon: <EditOutlined />,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
]}
|
||||
width={1080}
|
||||
tabs={[
|
||||
{
|
||||
key: 'images',
|
||||
label: (
|
||||
<span>
|
||||
<DatabaseOutlined style={{ marginRight: 8 }} />
|
||||
终端镜像
|
||||
</span>
|
||||
),
|
||||
content: (
|
||||
<div className="card-list">
|
||||
{/* 镜像列表内容 */}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
label: (
|
||||
<span>
|
||||
<UserOutlined style={{ marginRight: 8 }} />
|
||||
终端用户
|
||||
</span>
|
||||
),
|
||||
content: (
|
||||
<div className="card-list">
|
||||
{/* 用户列表内容 */}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InfoPanel data={selectedHost} fields={hostFields} />
|
||||
</DetailDrawer>
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
||||
import { Tag, Badge, Card } from 'antd'
|
||||
import { EditOutlined, DeleteOutlined, DatabaseOutlined, UserOutlined } from '@ant-design/icons'
|
||||
|
||||
function UserListPage() {
|
||||
const [showDetailDrawer, setShowDetailDrawer] = useState(false)
|
||||
const [selectedUser, setSelectedUser] = useState(null)
|
||||
|
||||
const userFields = [
|
||||
{ key: 'userName', label: '用户名', span: 6 },
|
||||
{ key: 'group', label: '用户分组', span: 6 },
|
||||
{ key: 'name', label: '姓名', span: 6 },
|
||||
{ key: 'grantedImages', label: '授权镜像', span: 6 },
|
||||
{ key: 'userType', label: '用户类型', span: 6 },
|
||||
{ key: 'grantedTerminals', label: '授权终端', span: 6 },
|
||||
{
|
||||
key: 'status',
|
||||
label: '启停用',
|
||||
span: 6,
|
||||
render: (value) => (
|
||||
<Tag color={value === 'enabled' ? 'green' : 'default'}>
|
||||
{value === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const detailTabs = [
|
||||
{
|
||||
key: 'terminals',
|
||||
label: (
|
||||
<span>
|
||||
<DesktopOutlined style={{ marginRight: 8 }} />
|
||||
授权终端
|
||||
</span>
|
||||
),
|
||||
content: (
|
||||
<div className="card-list">
|
||||
{selectedUser?.terminals?.map((terminal) => (
|
||||
<Card key={terminal.id}>
|
||||
<h4>{terminal.name}</h4>
|
||||
<p>IP: {terminal.ip}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'images',
|
||||
label: (
|
||||
<span>
|
||||
<DatabaseOutlined style={{ marginRight: 8 }} />
|
||||
授权镜像
|
||||
</span>
|
||||
),
|
||||
content: (
|
||||
<div className="card-list">
|
||||
{selectedUser?.images?.map((image) => (
|
||||
<Card key={image.id}>
|
||||
<h4>{image.name}</h4>
|
||||
<p>系统: {image.os}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 列表页面 */}
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
onRowClick={(record) => {
|
||||
setSelectedUser(record)
|
||||
setShowDetailDrawer(true)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 详情抽屉 */}
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
title={{
|
||||
text: selectedUser?.userName || '',
|
||||
badge: (
|
||||
<Tag color={selectedUser?.status === 'enabled' ? 'green' : 'default'}>
|
||||
{selectedUser?.status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
}}
|
||||
headerActions={[
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setShowDetailDrawer(false)
|
||||
handleEdit(selectedUser)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
setShowDetailDrawer(false)
|
||||
handleDelete(selectedUser)
|
||||
},
|
||||
},
|
||||
]}
|
||||
width={1080}
|
||||
tabs={detailTabs}
|
||||
>
|
||||
<InfoPanel
|
||||
data={selectedUser}
|
||||
fields={userFields}
|
||||
actions={[
|
||||
{ key: 'move', label: '转移分组', type: 'primary', onClick: () => console.log('转移分组') },
|
||||
{ key: 'reset', label: '重置密码', onClick: () => console.log('重置密码') },
|
||||
{ key: 'disable', label: '停用', onClick: () => console.log('停用') },
|
||||
]}
|
||||
/>
|
||||
</DetailDrawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 布局结构
|
||||
|
||||
抽屉采用固定头部、可滚动内容的布局:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 标题栏(固定,不滚动) │
|
||||
│ [关闭] [标题] [徽标] [操作按钮] │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ 内容区域(可滚动) │
|
||||
│ - children 主要内容 │
|
||||
│ - tabs 标签页(可选) │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件提供以下 CSS 类名供自定义样式:
|
||||
|
||||
- `.detail-drawer-content` - 抽屉内容容器
|
||||
- `.detail-drawer-header` - 顶部标题栏
|
||||
- `.detail-drawer-header-left` - 标题栏左侧区域
|
||||
- `.detail-drawer-header-right` - 标题栏右侧区域
|
||||
- `.detail-drawer-close-button` - 关闭按钮
|
||||
- `.detail-drawer-header-info` - 标题信息容器
|
||||
- `.detail-drawer-title-icon` - 标题图标
|
||||
- `.detail-drawer-title` - 标题文本
|
||||
- `.detail-drawer-badge` - 徽标容器
|
||||
- `.detail-drawer-scrollable-content` - 可滚动内容区域
|
||||
- `.detail-drawer-tabs` - 标签页容器
|
||||
- `.detail-drawer-tab-content` - 标签页内容
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **查看详细信息** - 点击列表行显示详细信息
|
||||
2. **多标签页详情** - 在详情中展示不同类型的关联数据
|
||||
3. **带快捷操作的详情** - 在详情顶部提供编辑、删除等操作
|
||||
4. **复杂数据展示** - 配合 InfoPanel、Card 等组件展示复杂信息
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 抽屉宽度默认 1080px,可根据内容调整,建议取值范围:720-1200px
|
||||
2. 标题栏固定在顶部,不随内容滚动,确保操作按钮始终可见
|
||||
3. `children` 内容区域会自动应用内边距,`tabs` 内容需要自行控制样式
|
||||
4. 操作按钮数量不宜过多,建议不超过 3 个
|
||||
5. 使用 `tabs` 时,第一个标签页默认激活
|
||||
6. 关闭抽屉时建议清空选中状态,避免下次打开时显示旧数据
|
||||
7. 配合 InfoPanel 使用时,InfoPanel 会自动处理内边距
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
# InfoPanel 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
信息展示面板组件,用于以网格布局展示数据的字段信息,支持自定义字段渲染和操作按钮。常用于详情页面或抽屉中展示结构化数据。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/InfoPanel/InfoPanel.jsx
|
||||
src/components/InfoPanel/InfoPanel.css
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| data | Object | 是 | - | 数据源对象 |
|
||||
| fields | Array<FieldConfig> | 否 | [] | 字段配置数组 |
|
||||
| actions | Array<ActionConfig> | 否 | [] | 操作按钮配置数组 |
|
||||
| gutter | Array<number> | 否 | [24, 16] | Grid 网格间距 [水平, 垂直] |
|
||||
|
||||
### FieldConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| key | string | 是 | 数据字段名 |
|
||||
| label | string | 是 | 字段显示标签 |
|
||||
| span | number | 否 | 网格占位份数(24栅格系统),默认 6 |
|
||||
| render | function(value, data) | 否 | 自定义渲染函数 |
|
||||
|
||||
### ActionConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| key | string | 是 | 按钮唯一标识 |
|
||||
| label | string | 是 | 按钮文本 |
|
||||
| icon | ReactNode | 否 | 按钮图标 |
|
||||
| type | string | 否 | 按钮类型(primary/default/dashed/text/link) |
|
||||
| danger | boolean | 否 | 是否为危险按钮 |
|
||||
| disabled | boolean | 否 | 是否禁用 |
|
||||
| onClick | function | 否 | 点击回调函数 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```jsx
|
||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
||||
|
||||
function UserDetail() {
|
||||
const userData = {
|
||||
userName: 'admin',
|
||||
name: '系统管理员',
|
||||
group: '管理员组',
|
||||
userType: '管理员',
|
||||
status: 'enabled',
|
||||
grantedTerminals: 8,
|
||||
grantedImages: 21,
|
||||
}
|
||||
|
||||
const userFields = [
|
||||
{ key: 'userName', label: '用户名', span: 6 },
|
||||
{ key: 'name', label: '姓名', span: 6 },
|
||||
{ key: 'group', label: '用户分组', span: 6 },
|
||||
{ key: 'userType', label: '用户类型', span: 6 },
|
||||
{ key: 'grantedTerminals', label: '授权终端', span: 6 },
|
||||
{ key: 'grantedImages', label: '授权镜像', span: 6 },
|
||||
]
|
||||
|
||||
return <InfoPanel data={userData} fields={userFields} />
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义字段渲染
|
||||
|
||||
```jsx
|
||||
import { Tag } from 'antd'
|
||||
|
||||
const userFields = [
|
||||
{ key: 'userName', label: '用户名', span: 6 },
|
||||
{ key: 'name', label: '姓名', span: 6 },
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
span: 6,
|
||||
render: (value) => (
|
||||
<Tag color={value === 'enabled' ? 'green' : 'default'}>
|
||||
{value === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: '描述',
|
||||
span: 18,
|
||||
render: (value) => value || '--', // 空值显示默认占位符
|
||||
},
|
||||
]
|
||||
|
||||
<InfoPanel data={userData} fields={userFields} />
|
||||
```
|
||||
|
||||
### 带操作按钮
|
||||
|
||||
```jsx
|
||||
import { LockOutlined } from '@ant-design/icons'
|
||||
|
||||
<InfoPanel
|
||||
data={userData}
|
||||
fields={userFields}
|
||||
actions={[
|
||||
{
|
||||
key: 'move',
|
||||
label: '转移分组',
|
||||
type: 'primary',
|
||||
onClick: () => console.log('转移分组'),
|
||||
},
|
||||
{
|
||||
key: 'blacklist',
|
||||
label: '加入黑名单',
|
||||
onClick: () => console.log('加入黑名单'),
|
||||
},
|
||||
{
|
||||
key: 'reset',
|
||||
label: '重置密码',
|
||||
onClick: () => console.log('重置密码'),
|
||||
},
|
||||
{
|
||||
key: 'disable',
|
||||
label: '停用',
|
||||
icon: <LockOutlined />,
|
||||
onClick: () => console.log('停用'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### 配合 DetailDrawer 使用
|
||||
|
||||
```jsx
|
||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
||||
import { Tag } from 'antd'
|
||||
|
||||
function UserListPage() {
|
||||
const [showDetailDrawer, setShowDetailDrawer] = useState(false)
|
||||
const [selectedUser, setSelectedUser] = useState(null)
|
||||
|
||||
const userFields = [
|
||||
{ key: 'userName', label: '用户名', span: 6 },
|
||||
{ key: 'group', label: '用户分组', span: 6 },
|
||||
{ key: 'name', label: '姓名', span: 6 },
|
||||
{ key: 'grantedImages', label: '授权镜像', span: 6 },
|
||||
{ key: 'userType', label: '用户类型', span: 6 },
|
||||
{ key: 'grantedTerminals', label: '授权终端', span: 6 },
|
||||
{
|
||||
key: 'status',
|
||||
label: '启停用',
|
||||
span: 6,
|
||||
render: (value) => (
|
||||
<Tag color={value === 'enabled' ? 'green' : 'default'}>
|
||||
{value === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ key: 'description', label: '描述', span: 18, render: () => '--' },
|
||||
]
|
||||
|
||||
return (
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
title={{
|
||||
text: selectedUser?.userName || '',
|
||||
}}
|
||||
>
|
||||
<InfoPanel
|
||||
data={selectedUser}
|
||||
fields={userFields}
|
||||
actions={[
|
||||
{ key: 'move', label: '转移分组', type: 'primary', onClick: () => console.log('转移分组') },
|
||||
{ key: 'blacklist', label: '加入黑名单', onClick: () => console.log('加入黑名单') },
|
||||
{ key: 'reset', label: '重置密码', onClick: () => console.log('重置密码') },
|
||||
{ key: 'disable', label: '停用', onClick: () => console.log('停用') },
|
||||
]}
|
||||
/>
|
||||
</DetailDrawer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 不同 span 值的布局
|
||||
|
||||
```jsx
|
||||
// 24栅格系统,一行总共24份
|
||||
const fields = [
|
||||
{ key: 'field1', label: '字段1', span: 6 }, // 占1/4宽度
|
||||
{ key: 'field2', label: '字段2', span: 6 }, // 占1/4宽度
|
||||
{ key: 'field3', label: '字段3', span: 6 }, // 占1/4宽度
|
||||
{ key: 'field4', label: '字段4', span: 6 }, // 占1/4宽度,满一行
|
||||
{ key: 'field5', label: '字段5', span: 8 }, // 占1/3宽度
|
||||
{ key: 'field6', label: '字段6', span: 8 }, // 占1/3宽度
|
||||
{ key: 'field7', label: '字段7', span: 8 }, // 占1/3宽度,满一行
|
||||
{ key: 'field8', label: '字段8', span: 12 }, // 占1/2宽度
|
||||
{ key: 'field9', label: '字段9', span: 12 }, // 占1/2宽度,满一行
|
||||
{ key: 'description', label: '描述', span: 24 }, // 占满整行
|
||||
]
|
||||
|
||||
<InfoPanel data={data} fields={fields} />
|
||||
```
|
||||
|
||||
### 访问完整数据对象
|
||||
|
||||
```jsx
|
||||
const fields = [
|
||||
{ key: 'userName', label: '用户名', span: 6 },
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态信息',
|
||||
span: 18,
|
||||
// render 函数的第二个参数是完整的数据对象
|
||||
render: (value, data) => (
|
||||
<div>
|
||||
<Tag color={value === 'enabled' ? 'green' : 'default'}>
|
||||
{value === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
授权终端:{data.grantedTerminals} 台
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
<InfoPanel data={userData} fields={fields} />
|
||||
```
|
||||
|
||||
### 自定义网格间距
|
||||
|
||||
```jsx
|
||||
// 默认间距 [24, 16]
|
||||
<InfoPanel
|
||||
data={userData}
|
||||
fields={userFields}
|
||||
gutter={[32, 24]} // 更大的间距
|
||||
/>
|
||||
|
||||
// 紧凑布局
|
||||
<InfoPanel
|
||||
data={userData}
|
||||
fields={userFields}
|
||||
gutter={[16, 12]} // 更小的间距
|
||||
/>
|
||||
```
|
||||
|
||||
## 布局说明
|
||||
|
||||
组件使用 Ant Design 的 24 栅格系统:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ span=6 span=6 span=6 span=6 │ 一行4列
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ span=8 span=8 span=8 │ 一行3列
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ span=12 span=12 │ 一行2列
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ span=24 │ 占满一行
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
常用 span 值:
|
||||
- `span=6` - 一行 4 列
|
||||
- `span=8` - 一行 3 列
|
||||
- `span=12` - 一行 2 列
|
||||
- `span=24` - 占满整行(适合描述、备注等长文本字段)
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件提供以下 CSS 类名供自定义样式:
|
||||
|
||||
- `.info-panel` - 组件根容器
|
||||
- `.info-panel-item` - 单个字段容器
|
||||
- `.info-panel-label` - 字段标签
|
||||
- `.info-panel-value` - 字段值
|
||||
- `.info-panel-actions` - 操作按钮区域
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **详情页信息展示** - 在详情抽屉或页面中展示对象的属性信息
|
||||
2. **用户信息展示** - 展示用户的基本信息和状态
|
||||
3. **设备信息展示** - 展示设备的配置和参数
|
||||
4. **订单信息展示** - 展示订单的详细信息
|
||||
5. **任何结构化数据展示** - 以标签-值形式展示的数据
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. `data` 为 `null` 或 `undefined` 时组件不渲染任何内容
|
||||
2. 字段 `span` 值总和建议为 24 的倍数以保持布局整齐
|
||||
3. 长文本字段(如描述、备注)建议使用 `span=24` 或 `span=18`
|
||||
4. `render` 函数可以返回任何 React 节点,包括组件、文本、HTML 等
|
||||
5. 操作按钮会显示在所有字段下方,建议不超过 6 个按钮
|
||||
6. 使用 `render` 函数时,第一个参数是字段值,第二个参数是完整数据对象
|
||||
7. 网格间距 `gutter` 的第一个值是水平间距,第二个值是垂直间距
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
# ListActionBar 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
列表操作栏组件,提供统一的操作按钮区、搜索框、高级筛选和刷新功能。常用于列表页面的顶部操作区域。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/ListActionBar/ListActionBar.jsx
|
||||
src/components/ListActionBar/ListActionBar.css
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| actions | Array<ActionConfig> | 否 | [] | 左侧操作按钮配置数组 |
|
||||
| search | SearchConfig | 否 | - | 搜索框配置对象 |
|
||||
| filter | FilterConfig | 否 | - | 高级筛选配置对象 |
|
||||
| showRefresh | boolean | 否 | false | 是否显示刷新按钮 |
|
||||
| onRefresh | function | 否 | - | 刷新按钮点击回调 |
|
||||
|
||||
### ActionConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| key | string | 是 | 按钮唯一标识 |
|
||||
| label | string | 是 | 按钮文本 |
|
||||
| icon | ReactNode | 否 | 按钮图标 |
|
||||
| type | string | 否 | 按钮类型(primary/default/dashed/text/link) |
|
||||
| disabled | boolean | 否 | 是否禁用 |
|
||||
| danger | boolean | 否 | 是否为危险按钮 |
|
||||
| onClick | function | 否 | 点击回调函数 |
|
||||
|
||||
### SearchConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| placeholder | string | 否 | 搜索框占位文本 |
|
||||
| width | number | 否 | 搜索框宽度,默认 280 |
|
||||
| value | string | 否 | 搜索框值(受控模式) |
|
||||
| onSearch | function(value: string) | 否 | 搜索回调 |
|
||||
| onChange | function(value: string) | 否 | 输入变化回调 |
|
||||
|
||||
### FilterConfig 配置项
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| content | ReactNode | 是 | 筛选面板内容 |
|
||||
| title | string | 否 | 筛选面板标题 |
|
||||
| visible | boolean | 否 | 控制面板显示/隐藏 |
|
||||
| onVisibleChange | function(visible: boolean) | 否 | 显示状态变化回调 |
|
||||
| selectedLabel | string | 否 | 筛选按钮显示的标签文本 |
|
||||
| isActive | boolean | 否 | 是否处于激活状态(高亮显示) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法(仅操作按钮)
|
||||
|
||||
```jsx
|
||||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新增',
|
||||
icon: <PlusOutlined />,
|
||||
type: 'primary',
|
||||
onClick: () => console.log('新增'),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '批量删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
disabled: selectedRowKeys.length === 0,
|
||||
onClick: handleBatchDelete,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### 带搜索功能
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
|
||||
function MyPage() {
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
|
||||
return (
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新增用户',
|
||||
icon: <PlusOutlined />,
|
||||
type: 'primary',
|
||||
onClick: () => console.log('新增'),
|
||||
},
|
||||
]}
|
||||
search={{
|
||||
placeholder: '搜索用户名或姓名',
|
||||
value: searchKeyword,
|
||||
onSearch: (value) => {
|
||||
console.log('搜索:', value)
|
||||
setSearchKeyword(value)
|
||||
},
|
||||
onChange: (value) => setSearchKeyword(value),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 带高级筛选
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import TreeFilterPanel from '../components/TreeFilterPanel/TreeFilterPanel'
|
||||
|
||||
function MyPage() {
|
||||
const [showFilterPopover, setShowFilterPopover] = useState(false)
|
||||
const [selectedGroup, setSelectedGroup] = useState(null)
|
||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||
|
||||
return (
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新增',
|
||||
icon: <PlusOutlined />,
|
||||
type: 'primary',
|
||||
onClick: () => console.log('新增'),
|
||||
},
|
||||
]}
|
||||
search={{
|
||||
placeholder: '搜索关键词',
|
||||
onSearch: handleSearch,
|
||||
}}
|
||||
filter={{
|
||||
content: (
|
||||
<TreeFilterPanel
|
||||
treeData={treeData}
|
||||
selectedKey={selectedGroup}
|
||||
onConfirm={handleConfirmFilter}
|
||||
onClear={handleClearFilter}
|
||||
/>
|
||||
),
|
||||
title: '高级筛选',
|
||||
visible: showFilterPopover,
|
||||
onVisibleChange: setShowFilterPopover,
|
||||
selectedLabel: selectedGroupName,
|
||||
isActive: !!selectedGroup,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 完整示例(所有功能)
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||||
import TreeFilterPanel from '../components/TreeFilterPanel/TreeFilterPanel'
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
|
||||
function UserListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [showFilterPopover, setShowFilterPopover] = useState(false)
|
||||
const [selectedGroup, setSelectedGroup] = useState(null)
|
||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||
|
||||
return (
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新增用户',
|
||||
icon: <PlusOutlined />,
|
||||
type: 'primary',
|
||||
onClick: handleAdd,
|
||||
},
|
||||
{
|
||||
key: 'batchDelete',
|
||||
label: '批量删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
disabled: selectedRowKeys.length === 0,
|
||||
onClick: handleBatchDelete,
|
||||
},
|
||||
]}
|
||||
search={{
|
||||
placeholder: '搜索用户名或姓名',
|
||||
value: searchKeyword,
|
||||
onSearch: handleSearch,
|
||||
onChange: handleSearch,
|
||||
}}
|
||||
filter={{
|
||||
content: (
|
||||
<TreeFilterPanel
|
||||
treeData={treeData}
|
||||
selectedKey={selectedGroup}
|
||||
onConfirm={handleConfirmFilter}
|
||||
onClear={handleClearFilter}
|
||||
/>
|
||||
),
|
||||
title: '高级筛选',
|
||||
visible: showFilterPopover,
|
||||
onVisibleChange: setShowFilterPopover,
|
||||
selectedLabel: selectedGroupName,
|
||||
isActive: !!selectedGroup,
|
||||
}}
|
||||
showRefresh
|
||||
onRefresh={() => console.log('刷新')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件提供以下 CSS 类名供自定义样式:
|
||||
|
||||
- `.list-action-bar` - 组件根容器
|
||||
- `.list-action-bar-left` - 左侧操作按钮区域
|
||||
- `.list-action-bar-right` - 右侧搜索筛选区域
|
||||
- `.filter-popover` - 筛选弹出层容器
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **列表页面顶部操作区** - 提供新增、批量操作等功能
|
||||
2. **带搜索的列表** - 快速搜索列表内容
|
||||
3. **带分组筛选的列表** - 通过树形结构筛选数据
|
||||
4. **需要刷新的列表** - 提供手动刷新功能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. `actions` 数组中的每个按钮必须提供唯一的 `key` 值
|
||||
2. 搜索框支持受控和非受控两种模式,推荐使用受控模式以便更好地管理状态
|
||||
3. 筛选功能的 `content` 可以是任意 React 组件,常配合 `TreeFilterPanel` 使用
|
||||
4. 当筛选激活时(`isActive: true`),筛选按钮会显示为 primary 类型以提示用户
|
||||
5. 左侧操作按钮不宜过多,建议不超过 5 个,过多的操作可以通过下拉菜单组织
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
# ListTable 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
列表表格组件,基于 Ant Design Table 组件封装,提供统一的表格样式、行选择、分页、滚动和行点击等功能。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/ListTable/ListTable.jsx
|
||||
src/components/ListTable/ListTable.css
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| columns | Array<ColumnConfig> | 是 | - | 表格列配置数组 |
|
||||
| dataSource | Array<Object> | 是 | - | 表格数据源 |
|
||||
| rowKey | string | 否 | 'id' | 行数据的唯一标识字段名 |
|
||||
| selectedRowKeys | Array<string\|number> | 否 | [] | 选中行的 key 数组 |
|
||||
| onSelectionChange | function(keys: Array) | 否 | - | 行选择变化回调 |
|
||||
| pagination | Object\|false | 否 | 默认配置 | 分页配置,false 表示不分页 |
|
||||
| scroll | Object | 否 | { x: 1200 } | 表格滚动配置 |
|
||||
| onRowClick | function(record: Object) | 否 | - | 行点击回调 |
|
||||
| selectedRow | Object | 否 | - | 当前选中的行数据对象 |
|
||||
| loading | boolean | 否 | false | 表格加载状态 |
|
||||
| className | string | 否 | '' | 自定义类名 |
|
||||
|
||||
### ColumnConfig 列配置
|
||||
|
||||
继承自 Ant Design Table 的列配置,常用属性:
|
||||
|
||||
| 属性名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| title | string\|ReactNode | 列标题 |
|
||||
| dataIndex | string | 数据字段名 |
|
||||
| key | string | 列唯一标识 |
|
||||
| width | number | 列宽度 |
|
||||
| align | 'left'\|'center'\|'right' | 对齐方式 |
|
||||
| fixed | 'left'\|'right' | 固定列 |
|
||||
| render | function(value, record, index) | 自定义渲染函数 |
|
||||
|
||||
### 默认分页配置
|
||||
|
||||
```javascript
|
||||
{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```jsx
|
||||
import ListTable from '../components/ListTable/ListTable'
|
||||
|
||||
function MyPage() {
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'userName',
|
||||
key: 'userName',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status) => (
|
||||
<Tag color={status === 'enabled' ? 'green' : 'default'}>
|
||||
{status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const dataSource = [
|
||||
{ id: 1, userName: 'admin', name: '管理员', status: 'enabled' },
|
||||
{ id: 2, userName: 'user', name: '张三', status: 'disabled' },
|
||||
]
|
||||
|
||||
return (
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 带行选择
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import ListTable from '../components/ListTable/ListTable'
|
||||
|
||||
function UserListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 显示选中的数量 */}
|
||||
<div>已选择 {selectedRowKeys.length} 项</div>
|
||||
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onSelectionChange={setSelectedRowKeys}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 带行点击和高亮
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import ListTable from '../components/ListTable/ListTable'
|
||||
|
||||
function UserListPage() {
|
||||
const [selectedUser, setSelectedUser] = useState(null)
|
||||
|
||||
const handleRowClick = (record) => {
|
||||
setSelectedUser(record)
|
||||
// 打开详情抽屉等操作
|
||||
setShowDetailDrawer(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
onRowClick={handleRowClick}
|
||||
selectedRow={selectedUser}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义分页
|
||||
|
||||
```jsx
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `总计 ${total} 条记录`,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### 禁用分页
|
||||
|
||||
```jsx
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
/>
|
||||
```
|
||||
|
||||
### 带加载状态
|
||||
|
||||
```jsx
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
function UserListPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [dataSource, setDataSource] = useState([])
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await api.fetchUsers()
|
||||
setDataSource(data)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 横向滚动(列较多时)
|
||||
|
||||
```jsx
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
scroll={{ x: 1600 }} // 内容宽度超过容器时出现横向滚动
|
||||
/>
|
||||
```
|
||||
|
||||
### 固定列
|
||||
|
||||
```jsx
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
fixed: 'left', // 固定在左侧
|
||||
},
|
||||
// ... 其他列
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right', // 固定在右侧
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" size="small">编辑</Button>
|
||||
<Button type="link" size="small" danger>删除</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
scroll={{ x: 1600 }}
|
||||
/>
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import ListTable from '../components/ListTable/ListTable'
|
||||
import { Tag, Button, Space } from 'antd'
|
||||
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
|
||||
function UserListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
const [selectedUser, setSelectedUser] = useState(null)
|
||||
const [showDetailDrawer, setShowDetailDrawer] = useState(false)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'userName',
|
||||
key: 'userName',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '用户分组',
|
||||
dataIndex: 'group',
|
||||
key: 'group',
|
||||
width: 150,
|
||||
render: (text) => <Tag color="blue">{text}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (status) => (
|
||||
<Tag color={status === 'enabled' ? 'green' : 'default'}>
|
||||
{status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="small" onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => handleDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const handleRowClick = (record) => {
|
||||
setSelectedUser(record)
|
||||
setShowDetailDrawer(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={filteredUsers}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onSelectionChange={setSelectedRowKeys}
|
||||
onRowClick={handleRowClick}
|
||||
selectedRow={selectedUser}
|
||||
scroll={{ x: 1400 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件提供以下 CSS 类名供自定义样式:
|
||||
|
||||
- `.list-table-container` - 表格容器
|
||||
- `.row-selected` - 选中行的类名
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **用户列表** - 显示和管理用户数据
|
||||
2. **设备列表** - 显示和管理设备信息
|
||||
3. **订单列表** - 显示订单数据
|
||||
4. **任何需要表格展示的数据列表**
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. `columns` 配置中的 `key` 必须唯一
|
||||
2. `dataSource` 中的每条数据必须有 `rowKey` 指定的唯一标识字段(默认为 `id`)
|
||||
3. 操作列中的点击事件需要使用 `e.stopPropagation()` 阻止事件冒泡,避免触发行点击
|
||||
4. 当列数较多时,建议设置合适的 `scroll.x` 值并固定首尾列
|
||||
5. `selectedRow` 用于高亮显示,`selectedRowKeys` 用于多选
|
||||
6. 分页的 `total` 值会自动根据 `dataSource.length` 计算
|
||||
7. 使用 `render` 函数时,要注意性能,避免在渲染函数中进行复杂计算
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# PageTitleBar 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
页面标题栏组件,用于显示页面的标题、描述信息、操作按钮和可选的展开/收起控制按钮。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/PageTitleBar/PageTitleBar.jsx
|
||||
src/components/PageTitleBar/PageTitleBar.css
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| title | string | 是 | - | 页面标题文本 |
|
||||
| badge | ReactNode | 否 | - | 标题右侧的徽章内容(如状态标签等) |
|
||||
| description | string | 否 | - | 页面描述文本,显示在标题下方 |
|
||||
| actions | ReactNode | 否 | - | 右侧操作按钮区域内容 |
|
||||
| showToggle | boolean | 否 | false | 是否显示展开/收起按钮 |
|
||||
| onToggle | function(expanded: boolean) | 否 | - | 展开/收起状态变化时的回调函数 |
|
||||
| defaultExpanded | boolean | 否 | true | 默认展开状态 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```jsx
|
||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<div>
|
||||
<PageTitleBar
|
||||
title="用户列表"
|
||||
description="管理系统用户,包括用户信息、权限和授权管理"
|
||||
/>
|
||||
{/* 页面内容 */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 带徽章的标题
|
||||
|
||||
```jsx
|
||||
import { Tag } from 'antd'
|
||||
|
||||
<PageTitleBar
|
||||
title="主机详情"
|
||||
badge={<Tag color="green">在线</Tag>}
|
||||
description="查看主机的详细信息和运行状态"
|
||||
/>
|
||||
```
|
||||
|
||||
### 带展开/收起功能
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||
|
||||
function MyPage() {
|
||||
const [showStatsPanel, setShowStatsPanel] = useState(true)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitleBar
|
||||
title="主机列表"
|
||||
description="查看和管理所有接入的主机终端"
|
||||
showToggle={true}
|
||||
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
||||
/>
|
||||
|
||||
{/* 可展开/收起的内容区域 */}
|
||||
{showStatsPanel && (
|
||||
<div className="stats-panel">
|
||||
{/* 统计面板内容 */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 带操作按钮
|
||||
|
||||
```jsx
|
||||
import { Button } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
|
||||
<PageTitleBar
|
||||
title="用户列表"
|
||||
description="管理系统用户"
|
||||
actions={
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
新增用户
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件提供以下 CSS 类名供自定义样式:
|
||||
|
||||
- `.page-title-bar` - 组件根容器
|
||||
- `.title-bar-content` - 内容容器
|
||||
- `.title-bar-left` - 左侧内容区域
|
||||
- `.title-bar-right` - 右侧内容区域
|
||||
- `.title-group` - 标题和徽章组合
|
||||
- `.page-title` - 标题文本
|
||||
- `.title-badge` - 徽章容器
|
||||
- `.page-description` - 描述文本
|
||||
- `.title-actions` - 操作按钮区域
|
||||
- `.toggle-button` - 展开/收起按钮
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **列表页面** - 显示列表页面的标题和描述
|
||||
2. **详情页面** - 显示详情页的标题和状态标签
|
||||
3. **带统计面板的页面** - 配合展开/收起功能控制统计信息的显示
|
||||
4. **需要快捷操作的页面** - 通过 actions 参数在标题栏添加常用操作按钮
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. `title` 参数为必填项,建议简洁明了
|
||||
2. 当使用 `showToggle` 时,建议同时提供 `onToggle` 回调以响应状态变化
|
||||
3. `badge` 参数支持任何 React 节点,常用的如 Ant Design 的 Tag、Badge 组件
|
||||
4. `actions` 区域不建议放置过多按钮,以保持界面简洁
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
# 组件文档目录
|
||||
|
||||
## 概述
|
||||
|
||||
本目录包含 Nex Design 系统所有主要组件的详细文档,包括组件说明、参数配置、使用示例等。
|
||||
|
||||
## 组件列表
|
||||
|
||||
### 页面布局组件
|
||||
|
||||
1. **[PageTitleBar](./PageTitleBar.md)** - 页面标题栏组件
|
||||
- 显示页面标题、描述和操作按钮
|
||||
- 支持展开/收起功能
|
||||
- 适用于所有页面的顶部区域
|
||||
|
||||
### 列表相关组件
|
||||
|
||||
2. **[ListActionBar](./ListActionBar.md)** - 列表操作栏组件
|
||||
- 提供操作按钮、搜索、筛选功能
|
||||
- 适用于列表页面的顶部操作区
|
||||
|
||||
3. **[TreeFilterPanel](./TreeFilterPanel.md)** - 树形筛选面板组件
|
||||
- 树形结构的数据筛选
|
||||
- 支持搜索和多级展开
|
||||
- 配合 ListActionBar 使用
|
||||
|
||||
4. **[ListTable](./ListTable.md)** - 列表表格组件
|
||||
- 统一的表格样式和交互
|
||||
- 支持行选择、分页、排序
|
||||
- 适用于所有列表页面
|
||||
|
||||
### 详情展示组件
|
||||
|
||||
5. **[DetailDrawer](./DetailDrawer.md)** - 详情抽屉组件
|
||||
- 从右侧滑出的详情面板
|
||||
- 支持标签页和操作按钮
|
||||
- 固定头部,内容可滚动
|
||||
|
||||
6. **[InfoPanel](./InfoPanel.md)** - 信息展示面板组件
|
||||
- 网格布局展示结构化数据
|
||||
- 支持自定义字段渲染
|
||||
- 配合 DetailDrawer 使用
|
||||
|
||||
### 交互反馈组件
|
||||
|
||||
7. **[ConfirmDialog](./ConfirmDialog.md)** - 确认对话框组件
|
||||
- 提供统一的确认对话框样式
|
||||
- 支持删除、警告、通用确认等场景
|
||||
- 支持异步操作
|
||||
|
||||
8. **[Toast](./Toast.md)** - 通知反馈组件
|
||||
- 操作完成后的提示信息
|
||||
- 支持成功、错误、警告、信息四种类型
|
||||
- 从右上角滑出,自动消失
|
||||
|
||||
## 组件关系图
|
||||
|
||||
```
|
||||
页面结构层次:
|
||||
|
||||
┌─────────────────────────────────────────┐
|
||||
│ PageTitleBar (页面标题栏) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ ListActionBar (操作栏) │
|
||||
│ ├─ 操作按钮 │
|
||||
│ ├─ 搜索框 │
|
||||
│ └─ TreeFilterPanel (筛选面板) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ ListTable (数据表格) │
|
||||
│ └─ 点击行 → DetailDrawer │
|
||||
├─────────────────────────────────────────┤
|
||||
│ DetailDrawer (详情抽屉) │
|
||||
│ ├─ InfoPanel (基本信息) │
|
||||
│ └─ Tabs (关联数据标签页) │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
交互反馈:
|
||||
操作 → ConfirmDialog (确认) → Toast (结果反馈)
|
||||
```
|
||||
|
||||
## 组件组合示例
|
||||
|
||||
### 标准列表页面
|
||||
|
||||
```jsx
|
||||
<PageTitleBar title="用户列表" description="..." />
|
||||
|
||||
<ListActionBar
|
||||
actions={[...]}
|
||||
search={{...}}
|
||||
filter={{
|
||||
content: <TreeFilterPanel {...} />
|
||||
}}
|
||||
/>
|
||||
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
onRowClick={showDetail}
|
||||
/>
|
||||
|
||||
<DetailDrawer visible={showDrawer}>
|
||||
<InfoPanel data={selectedItem} fields={fields} />
|
||||
</DetailDrawer>
|
||||
```
|
||||
|
||||
### 删除操作流程
|
||||
|
||||
```jsx
|
||||
// 1. 点击删除按钮
|
||||
<Button onClick={() => handleDelete(record)}>删除</Button>
|
||||
|
||||
// 2. 显示确认对话框
|
||||
ConfirmDialog.delete({
|
||||
itemName: record.name,
|
||||
onOk: async () => {
|
||||
// 3. 执行删除
|
||||
await api.delete(record.id)
|
||||
// 4. 显示结果反馈
|
||||
Toast.success('删除成功')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 开始使用
|
||||
|
||||
1. 查看对应组件的详细文档
|
||||
2. 了解组件的参数配置
|
||||
3. 参考示例代码
|
||||
4. 根据实际需求调整参数
|
||||
|
||||
### 设计原则
|
||||
|
||||
- **一致性** - 所有组件使用统一的设计语言
|
||||
- **可复用** - 组件高度封装,易于复用
|
||||
- **可配置** - 提供丰富的配置选项
|
||||
- **易用性** - API 设计简洁直观
|
||||
|
||||
### 技术栈
|
||||
|
||||
- React 18
|
||||
- Ant Design 5.x
|
||||
- CSS Modules
|
||||
|
||||
## 更新记录
|
||||
|
||||
- 2025-11-04: 初始版本,包含 8 个核心组件文档
|
||||
|
|
@ -0,0 +1,398 @@
|
|||
# Toast 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
通知反馈组件,基于 Ant Design notification 封装,用于操作完成后向用户展示反馈信息。通知从右上角滑出,默认 3 秒后自动消失,最多同时显示 3 条通知。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/Toast/Toast.jsx
|
||||
```
|
||||
|
||||
## 全局配置
|
||||
|
||||
```javascript
|
||||
notification.config({
|
||||
placement: 'topRight', // 通知位置:右上角
|
||||
top: 24, // 距离顶部 24px
|
||||
duration: 3, // 默认显示 3 秒
|
||||
maxCount: 3, // 最多同时显示 3 条
|
||||
})
|
||||
```
|
||||
|
||||
## API 方法
|
||||
|
||||
组件以静态方法的形式提供,无需实例化,直接调用即可。
|
||||
|
||||
### Toast.success()
|
||||
|
||||
显示成功通知(绿色图标)。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| message | string | 是 | - | 主要消息内容 |
|
||||
| description | string | 否 | '' | 详细描述 |
|
||||
| duration | number | 否 | 3 | 显示时长(秒),0 表示不自动关闭 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
import Toast from '../components/Toast/Toast'
|
||||
|
||||
// 简单成功提示
|
||||
Toast.success('操作成功')
|
||||
|
||||
// 带详细描述
|
||||
Toast.success('删除成功', '用户 "admin" 已成功删除')
|
||||
|
||||
// 自定义显示时长(5秒)
|
||||
Toast.success('保存成功', '您的设置已保存', 5)
|
||||
|
||||
// 不自动关闭
|
||||
Toast.success('操作成功', '请注意后续操作', 0)
|
||||
```
|
||||
|
||||
### Toast.error()
|
||||
|
||||
显示错误通知(红色图标)。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| message | string | 是 | - | 主要消息内容 |
|
||||
| description | string | 否 | '' | 详细描述 |
|
||||
| duration | number | 否 | 3 | 显示时长(秒) |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
// 简单错误提示
|
||||
Toast.error('操作失败')
|
||||
|
||||
// 带错误详情
|
||||
Toast.error('删除失败', '该用户下还有关联数据,无法删除')
|
||||
|
||||
// API 错误处理
|
||||
try {
|
||||
await api.deleteUser(userId)
|
||||
Toast.success('删除成功')
|
||||
} catch (error) {
|
||||
Toast.error('删除失败', error.message)
|
||||
}
|
||||
```
|
||||
|
||||
### Toast.warning()
|
||||
|
||||
显示警告通知(橙色图标)。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| message | string | 是 | - | 主要消息内容 |
|
||||
| description | string | 否 | '' | 详细描述 |
|
||||
| duration | number | 否 | 3 | 显示时长(秒) |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
// 警告提示
|
||||
Toast.warning('操作警告', '此操作可能影响系统稳定性')
|
||||
|
||||
// 权限警告
|
||||
Toast.warning('权限不足', '您没有执行此操作的权限')
|
||||
|
||||
// 数据警告
|
||||
Toast.warning('数据异常', '检测到部分数据可能不完整')
|
||||
```
|
||||
|
||||
### Toast.info()
|
||||
|
||||
显示信息通知(蓝色图标)。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| message | string | 是 | - | 主要消息内容 |
|
||||
| description | string | 否 | '' | 详细描述 |
|
||||
| duration | number | 否 | 3 | 显示时长(秒) |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
// 信息提示
|
||||
Toast.info('系统提示', '系统将在5分钟后进行维护')
|
||||
|
||||
// 操作提示
|
||||
Toast.info('导入中', '正在导入数据,请稍候...')
|
||||
|
||||
// 功能提示
|
||||
Toast.info('新功能上线', '我们上线了新的数据导出功能')
|
||||
```
|
||||
|
||||
### Toast.custom()
|
||||
|
||||
显示自定义通知,支持所有 Ant Design notification 配置。
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| config | Object | 是 | 完整的 notification 配置对象 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```jsx
|
||||
import { Button } from 'antd'
|
||||
|
||||
// 带操作按钮的通知
|
||||
Toast.custom({
|
||||
message: '新版本可用',
|
||||
description: '发现新版本 v2.0.0,是否立即更新?',
|
||||
duration: 0,
|
||||
btn: (
|
||||
<Button type="primary" size="small" onClick={() => {
|
||||
console.log('更新')
|
||||
notification.close('update-key')
|
||||
}}>
|
||||
立即更新
|
||||
</Button>
|
||||
),
|
||||
key: 'update-key',
|
||||
})
|
||||
|
||||
// 自定义图标和样式
|
||||
Toast.custom({
|
||||
message: '自定义通知',
|
||||
description: '这是一个自定义样式的通知',
|
||||
icon: <StarOutlined style={{ color: '#faad14' }} />,
|
||||
style: {
|
||||
background: '#fffbe6',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## 完整使用示例
|
||||
|
||||
### 配合 ConfirmDialog 使用
|
||||
|
||||
```jsx
|
||||
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||
import Toast from '../components/Toast/Toast'
|
||||
|
||||
const handleDeleteUser = (record) => {
|
||||
ConfirmDialog.delete({
|
||||
itemName: `用户名:${record.userName}`,
|
||||
itemInfo: `姓名:${record.name}`,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newUsers = filteredUsers.filter((u) => u.id !== record.id)
|
||||
setFilteredUsers(newUsers)
|
||||
resolve()
|
||||
Toast.success('删除成功', `用户 "${record.userName}" 已成功删除`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 批量操作反馈
|
||||
|
||||
```jsx
|
||||
const handleBatchDelete = () => {
|
||||
ConfirmDialog.batchDelete({
|
||||
count: selectedRowKeys.length,
|
||||
items: selectedUsers,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const count = selectedRowKeys.length
|
||||
const newUsers = filteredUsers.filter((u) => !selectedRowKeys.includes(u.id))
|
||||
setFilteredUsers(newUsers)
|
||||
setSelectedRowKeys([])
|
||||
resolve()
|
||||
Toast.success('批量删除成功', `已成功删除 ${count} 个用户`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 异步操作反馈
|
||||
|
||||
```jsx
|
||||
const handleSaveUser = async (userData) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await api.updateUser(userData)
|
||||
setShowEditDrawer(false)
|
||||
Toast.success('保存成功', '用户信息已更新')
|
||||
fetchUsers() // 重新加载列表
|
||||
} catch (error) {
|
||||
Toast.error('保存失败', error.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多步骤操作反馈
|
||||
|
||||
```jsx
|
||||
const handleImportData = async () => {
|
||||
try {
|
||||
Toast.info('导入开始', '正在验证数据格式...')
|
||||
|
||||
await api.validateData()
|
||||
Toast.info('验证通过', '正在导入数据...')
|
||||
|
||||
const result = await api.importData()
|
||||
Toast.success('导入完成', `成功导入 ${result.count} 条数据`)
|
||||
|
||||
} catch (error) {
|
||||
if (error.type === 'validation') {
|
||||
Toast.warning('验证失败', error.message)
|
||||
} else {
|
||||
Toast.error('导入失败', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 表单提交反馈
|
||||
|
||||
```jsx
|
||||
const handleSubmit = async (values) => {
|
||||
try {
|
||||
await api.createUser(values)
|
||||
Toast.success('创建成功', `用户 "${values.userName}" 已成功创建`)
|
||||
setShowEditDrawer(false)
|
||||
fetchUsers()
|
||||
} catch (error) {
|
||||
if (error.code === 'DUPLICATE') {
|
||||
Toast.warning('用户名已存在', '请使用其他用户名')
|
||||
} else {
|
||||
Toast.error('创建失败', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 带按钮的持久通知
|
||||
|
||||
```jsx
|
||||
const showUpdateNotification = () => {
|
||||
const key = `update-${Date.now()}`
|
||||
|
||||
Toast.custom({
|
||||
message: '发现新版本',
|
||||
description: '系统发现新版本 v2.0.0,建议立即更新',
|
||||
duration: 0, // 不自动关闭
|
||||
key,
|
||||
btn: (
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => notification.close(key)}
|
||||
>
|
||||
稍后
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
notification.close(key)
|
||||
handleUpdate()
|
||||
}}
|
||||
>
|
||||
立即更新
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **操作成功反馈** - 增删改操作成功后的提示
|
||||
2. **操作失败反馈** - 操作失败时显示错误信息
|
||||
3. **警告提示** - 权限不足、数据异常等警告
|
||||
4. **信息通知** - 系统公告、功能提示等
|
||||
5. **进度通知** - 多步骤操作的进度反馈
|
||||
6. **版本更新通知** - 带操作按钮的持久通知
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 消息文案
|
||||
|
||||
```jsx
|
||||
// 好的做法:简洁明了
|
||||
Toast.success('删除成功', '用户已删除')
|
||||
|
||||
// 避免:冗余重复
|
||||
Toast.success('删除成功', '删除用户成功') // ❌
|
||||
```
|
||||
|
||||
### 显示时长
|
||||
|
||||
```jsx
|
||||
// 简单提示:3秒(默认)
|
||||
Toast.success('保存成功')
|
||||
|
||||
// 重要信息:5秒
|
||||
Toast.warning('权限不足', '请联系管理员', 5)
|
||||
|
||||
// 需要用户操作:不自动关闭
|
||||
Toast.custom({
|
||||
message: '需要您的确认',
|
||||
description: '...',
|
||||
duration: 0,
|
||||
btn: <Button>...</Button>,
|
||||
})
|
||||
```
|
||||
|
||||
### 类型选择
|
||||
|
||||
```jsx
|
||||
// 成功:操作完成
|
||||
Toast.success('保存成功')
|
||||
|
||||
// 错误:操作失败、异常
|
||||
Toast.error('保存失败', error.message)
|
||||
|
||||
// 警告:权限、数据问题
|
||||
Toast.warning('权限不足')
|
||||
|
||||
// 信息:提示、公告
|
||||
Toast.info('系统维护通知')
|
||||
```
|
||||
|
||||
## 样式特性
|
||||
|
||||
- **圆角设计**:8px 圆角,现代化视觉效果
|
||||
- **阴影效果**:0 4px 12px rgba(0, 0, 0, 0.15)
|
||||
- **彩色图标**:
|
||||
- 成功:绿色 (#52c41a)
|
||||
- 错误:红色 (#ff4d4f)
|
||||
- 警告:橙色 (#faad14)
|
||||
- 信息:蓝色 (#1677ff)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 通知从右上角滑出,默认 3 秒后自动消失
|
||||
2. 最多同时显示 3 条通知,超出的会排队等待
|
||||
3. `duration: 0` 表示通知不自动关闭,需手动关闭或调用 API 关闭
|
||||
4. 通知内容不宜过长,建议 message 不超过 20 字,description 不超过 50 字
|
||||
5. 频繁操作时避免连续显示大量通知,可以考虑合并提示
|
||||
6. 配合 ConfirmDialog 使用时,在 `onOk` 回调中显示操作结果
|
||||
7. 错误通知建议显示具体错误信息,帮助用户排查问题
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
# TreeFilterPanel 组件
|
||||
|
||||
## 组件说明
|
||||
|
||||
树形筛选面板组件,用于在弹出层中展示树形结构的筛选选项,支持树形选择、搜索、确认和清除操作。常配合 ListActionBar 组件使用。
|
||||
|
||||
## 组件位置
|
||||
|
||||
```
|
||||
src/components/TreeFilterPanel/TreeFilterPanel.jsx
|
||||
src/components/TreeFilterPanel/TreeFilterPanel.css
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| treeData | Array<TreeNode> | 是 | - | 树形数据数组 |
|
||||
| selectedKey | string | 否 | - | 当前选中的节点 key(已确认的选择) |
|
||||
| tempSelectedKey | string | 否 | - | 临时选中的节点 key(未确认) |
|
||||
| treeTitle | string | 否 | '分组筛选' | 树形选择器的标题 |
|
||||
| onSelect | function(key: string) | 否 | - | 节点选择变化回调 |
|
||||
| onConfirm | function | 否 | - | 确认筛选按钮回调 |
|
||||
| onClear | function | 否 | - | 清除筛选按钮回调 |
|
||||
| placeholder | string | 否 | '请选择分组进行筛选' | 未选择时的占位提示文本 |
|
||||
|
||||
### TreeNode 数据结构
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| key | string | 是 | 节点唯一标识 |
|
||||
| title | string | 是 | 节点显示文本 |
|
||||
| children | Array<TreeNode> | 否 | 子节点数组 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import TreeFilterPanel from '../components/TreeFilterPanel/TreeFilterPanel'
|
||||
|
||||
function MyPage() {
|
||||
const [selectedGroup, setSelectedGroup] = useState(null)
|
||||
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
|
||||
|
||||
const treeData = [
|
||||
{
|
||||
key: '1',
|
||||
title: '全部用户',
|
||||
children: [
|
||||
{
|
||||
key: '1-1',
|
||||
title: '管理员组',
|
||||
children: [
|
||||
{ key: '1-1-1', title: '系统管理员' },
|
||||
{ key: '1-1-2', title: '安全管理员' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '1-2',
|
||||
title: '部门用户',
|
||||
children: [
|
||||
{ key: '1-2-1', title: '研发部' },
|
||||
{ key: '1-2-2', title: '产品部' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const handleConfirm = () => {
|
||||
setSelectedGroup(tempSelectedGroup)
|
||||
// 执行筛选逻辑
|
||||
filterData(tempSelectedGroup)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setTempSelectedGroup(null)
|
||||
setSelectedGroup(null)
|
||||
// 清除筛选
|
||||
filterData(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeFilterPanel
|
||||
treeData={treeData}
|
||||
selectedKey={selectedGroup}
|
||||
tempSelectedKey={tempSelectedGroup}
|
||||
treeTitle="用户分组"
|
||||
onSelect={setTempSelectedGroup}
|
||||
onConfirm={handleConfirm}
|
||||
onClear={handleClear}
|
||||
placeholder="请选择用户分组进行筛选"
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 配合 ListActionBar 使用
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||||
import TreeFilterPanel from '../components/TreeFilterPanel/TreeFilterPanel'
|
||||
|
||||
function UserListPage() {
|
||||
const [showFilterPopover, setShowFilterPopover] = useState(false)
|
||||
const [selectedGroup, setSelectedGroup] = useState(null)
|
||||
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
|
||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||
|
||||
// 将原始数据转换为树形数据格式
|
||||
const convertTreeData = (nodes) => {
|
||||
return nodes.map((node) => ({
|
||||
title: node.name,
|
||||
key: node.id,
|
||||
children: node.children ? convertTreeData(node.children) : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
const treeData = convertTreeData(userData.userGroups)
|
||||
|
||||
const handleConfirmFilter = () => {
|
||||
setSelectedGroup(tempSelectedGroup)
|
||||
|
||||
// 查找节点名称
|
||||
const findGroupName = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node.name
|
||||
if (node.children) {
|
||||
const found = findGroupName(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const groupName = tempSelectedGroup
|
||||
? findGroupName(userData.userGroups, tempSelectedGroup)
|
||||
: ''
|
||||
setSelectedGroupName(groupName)
|
||||
|
||||
// 执行筛选
|
||||
filterUsers(searchKeyword, tempSelectedGroup)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
|
||||
const handleClearFilter = () => {
|
||||
setTempSelectedGroup(null)
|
||||
setSelectedGroup(null)
|
||||
setSelectedGroupName('')
|
||||
filterUsers(searchKeyword, null)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新增用户',
|
||||
type: 'primary',
|
||||
onClick: handleAdd,
|
||||
},
|
||||
]}
|
||||
search={{
|
||||
placeholder: '搜索用户',
|
||||
onSearch: handleSearch,
|
||||
}}
|
||||
filter={{
|
||||
content: (
|
||||
<TreeFilterPanel
|
||||
treeData={treeData}
|
||||
selectedKey={selectedGroup}
|
||||
tempSelectedKey={tempSelectedGroup}
|
||||
treeTitle="用户分组"
|
||||
onSelect={setTempSelectedGroup}
|
||||
onConfirm={handleConfirmFilter}
|
||||
onClear={handleClearFilter}
|
||||
placeholder="请选择用户分组进行筛选"
|
||||
/>
|
||||
),
|
||||
title: '高级筛选',
|
||||
visible: showFilterPopover,
|
||||
onVisibleChange: (visible) => {
|
||||
setShowFilterPopover(visible)
|
||||
if (visible) {
|
||||
// 打开时同步临时选择
|
||||
setTempSelectedGroup(selectedGroup)
|
||||
}
|
||||
},
|
||||
selectedLabel: selectedGroupName,
|
||||
isActive: !!selectedGroup,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 从 JSON 数据转换树形结构
|
||||
|
||||
```jsx
|
||||
// userData.json 中的数据格式
|
||||
{
|
||||
"userGroups": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "全部用户",
|
||||
"children": [
|
||||
{
|
||||
"id": "1-1",
|
||||
"name": "管理员组",
|
||||
"children": [
|
||||
{ "id": "1-1-1", "name": "系统管理员" },
|
||||
{ "id": "1-1-2", "name": "安全管理员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 转换函数
|
||||
const convertTreeData = (nodes) => {
|
||||
return nodes.map((node) => ({
|
||||
title: node.name, // 映射 name 到 title
|
||||
key: node.id, // 映射 id 到 key
|
||||
children: node.children ? convertTreeData(node.children) : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
const treeData = convertTreeData(userData.userGroups)
|
||||
```
|
||||
|
||||
## 组件特性
|
||||
|
||||
### 自动展开所有节点
|
||||
|
||||
组件会在数据加载时自动展开所有树节点,方便用户查看完整的层级结构。
|
||||
|
||||
```javascript
|
||||
// 组件内部实现
|
||||
useEffect(() => {
|
||||
if (treeData && treeData.length > 0) {
|
||||
setExpandedKeys(getAllKeys(treeData))
|
||||
}
|
||||
}, [treeData])
|
||||
```
|
||||
|
||||
### 显示当前选择
|
||||
|
||||
组件顶部会显示当前选中的节点,并提供关闭按钮快速清除选择:
|
||||
|
||||
```jsx
|
||||
{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>
|
||||
)}
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件提供以下 CSS 类名供自定义样式:
|
||||
|
||||
- `.tree-filter-panel` - 组件根容器
|
||||
- `.tree-filter-selected` - 已选择区域
|
||||
- `.tree-filter-tag` - 选中标签容器
|
||||
- `.tree-filter-label` - 标签文本
|
||||
- `.tree-filter-placeholder` - 占位提示
|
||||
- `.tree-filter-container` - 树形选择器容器
|
||||
- `.tree-filter-header` - 树形选择器标题
|
||||
- `.tree-filter-actions` - 操作按钮区域
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **用户分组筛选** - 按用户组织架构筛选用户列表
|
||||
2. **终端分组筛选** - 按终端分组筛选设备列表
|
||||
3. **部门筛选** - 按部门层级筛选数据
|
||||
4. **分类筛选** - 任何需要树形层级筛选的场景
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. `treeData` 必须符合 Ant Design Tree 组件的数据格式要求
|
||||
2. 需要区分 `selectedKey`(已确认)和 `tempSelectedKey`(临时选择),点击确认后才更新 `selectedKey`
|
||||
3. 建议在打开筛选面板时,将 `selectedKey` 同步到 `tempSelectedKey`,以便用户看到当前的选择状态
|
||||
4. 点击清除按钮会同时清除临时选择和已确认选择
|
||||
5. 组件会自动展开所有节点,如果数据量很大,可能需要考虑性能优化
|
||||
6. 树节点的 `key` 必须唯一,通常使用 ID 字段
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# Nex Design - 抽屉(Drawer)宽度标准
|
||||
|
||||
## 📐 抽屉宽度定义
|
||||
|
||||
为了保持设计一致性,我们定义了三种标准抽屉宽度模式:
|
||||
|
||||
### 1. 小型抽屉 (Small) - 480px
|
||||
**适用场景:**
|
||||
- 简单的信息展示
|
||||
- 少量字段的表单(1-3个字段)
|
||||
- 快速操作面板
|
||||
- 通知详情
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={480} ... />
|
||||
```
|
||||
|
||||
### 2. 中型抽屉 (Medium) - 720px ⭐️ 推荐
|
||||
**适用场景:**
|
||||
- 详细信息展示(如主机详情)
|
||||
- 中等复杂度的表单(4-10个字段)
|
||||
- 数据编辑面板
|
||||
- 配置设置
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={720} ... />
|
||||
```
|
||||
**当前主机列表页面使用此宽度模式**
|
||||
|
||||
### 3. 大型抽屉 (Large) - 1080px
|
||||
**适用场景:**
|
||||
- 复杂的多步骤表单
|
||||
- 需要并排展示多列信息
|
||||
- 包含图表或复杂可视化内容
|
||||
- 嵌套子表格或列表
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={1080} ... />
|
||||
```
|
||||
|
||||
## 📱 响应式建议
|
||||
|
||||
```css
|
||||
/* 移动端自适应 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-drawer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端 */
|
||||
@media (max-width: 1024px) {
|
||||
.ant-drawer-large {
|
||||
width: 90% !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 使用建议
|
||||
|
||||
1. **优先选择中型抽屉(720px)**:适合大多数场景,既不会太窄导致内容拥挤,也不会太宽浪费空间。
|
||||
|
||||
2. **避免自定义宽度**:保持使用标准宽度,确保整个系统的一致性。
|
||||
|
||||
3. **移动端优先**:在响应式设计中,小屏幕设备应使用全宽抽屉。
|
||||
|
||||
4. **内容决定宽度**:根据内容复杂度选择合适的宽度,而不是随意设置。
|
||||
|
||||
## 🎨 设计原则
|
||||
|
||||
- **一致性**:相同类型的页面使用相同宽度的抽屉
|
||||
- **可读性**:确保内容在抽屉中有足够的呼吸空间
|
||||
- **响应式**:考虑不同设备的显示效果
|
||||
- **用户体验**:抽屉不应遮挡主要内容过多
|
||||
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
|
|
@ -10,24 +10,28 @@
|
|||
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"antd": "^5.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"antd": "^5.12.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"@ant-design/icons": "^5.2.6"
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"vite": "^5.0.8",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"postcss": "^8.4.32",
|
||||
"autoprefixer": "^10.4.16"
|
||||
"tailwindcss": "^3.3.6",
|
||||
"vite": "^5.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import OverviewPage from './pages/OverviewPage'
|
|||
import HostListPage from './pages/HostListPage'
|
||||
import UserListPage from './pages/UserListPage'
|
||||
import ImageListPage from './pages/ImageListPage'
|
||||
import DocsPage from './pages/DocsPage'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
|
@ -14,6 +15,7 @@ function App() {
|
|||
<Route path="/host/list" element={<HostListPage />} />
|
||||
<Route path="/user/list" element={<UserListPage />} />
|
||||
<Route path="/image/list" element={<ImageListPage />} />
|
||||
<Route path="/design" element={<DocsPage />} />
|
||||
{/* 其他路由将在后续添加 */}
|
||||
</Routes>
|
||||
</MainLayout>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
/* 详情抽屉容器 */
|
||||
.detail-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部信息区域 - 固定不滚动 */
|
||||
.detail-drawer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-drawer-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-drawer-close-button {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-drawer-close-button:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.detail-drawer-header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-drawer-title-icon {
|
||||
font-size: 18px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.detail-drawer-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.detail-drawer-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-drawer-header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 可滚动内容区域 */
|
||||
.detail-drawer-scrollable-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 标签页区域 */
|
||||
.detail-drawer-tabs {
|
||||
background: #ffffff;
|
||||
padding-top: 16px;
|
||||
padding-left: 12px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs-content-holder) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs-nav) {
|
||||
padding: 0;
|
||||
margin: 0 24px;
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs-nav::before) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs-tab) {
|
||||
padding: 12px 0;
|
||||
margin: 0 32px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs-tab:first-child) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs-tab-active .ant-tabs-tab-btn) {
|
||||
color: #d946ef;
|
||||
}
|
||||
|
||||
.detail-drawer-tabs :global(.ant-tabs-ink-bar) {
|
||||
background: #d946ef;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.detail-drawer-tab-content {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { Drawer, Button, Space, Tabs } from 'antd'
|
||||
import { CloseOutlined } from '@ant-design/icons'
|
||||
import './DetailDrawer.css'
|
||||
|
||||
/**
|
||||
* 详情抽屉组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.visible - 是否显示抽屉
|
||||
* @param {Function} props.onClose - 关闭回调
|
||||
* @param {Object} props.title - 标题配置
|
||||
* @param {string} props.title.text - 标题文本
|
||||
* @param {ReactNode} props.title.badge - 状态徽标(可选)
|
||||
* @param {ReactNode} props.title.icon - 图标(可选)
|
||||
* @param {Array} props.headerActions - 顶部操作按钮
|
||||
* @param {number} props.width - 抽屉宽度
|
||||
* @param {ReactNode} props.children - 主要内容
|
||||
* @param {Array} props.tabs - 标签页配置(可选)
|
||||
*/
|
||||
function DetailDrawer({
|
||||
visible,
|
||||
onClose,
|
||||
title,
|
||||
headerActions = [],
|
||||
width = 1080,
|
||||
children,
|
||||
tabs,
|
||||
}) {
|
||||
return (
|
||||
<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
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/* 信息面板 */
|
||||
.info-panel {
|
||||
padding: 6px 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 信息区域容器 */
|
||||
.info-panel > :global(.ant-row) {
|
||||
padding: 32px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.info-panel-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info-panel-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 添加底部装饰条 */
|
||||
.info-panel-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
bottom: -1px;
|
||||
width: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #1677ff 0%, #4096ff 100%);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.info-panel-item:hover {
|
||||
background: linear-gradient(90deg, #f0f7ff 0%, transparent 100%);
|
||||
padding-left: 10px;
|
||||
padding-right: 16px;
|
||||
margin-left: -12px;
|
||||
margin-right: -16px;
|
||||
border-radius: 8px;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.info-panel-item:hover::before {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.info-panel-label {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-panel-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 操作按钮区 */
|
||||
.info-panel-actions {
|
||||
padding: 24px 32px;
|
||||
background: linear-gradient(to bottom, #fafafa 0%, #f5f5f5 100%);
|
||||
border-top: 2px solid #e8e8e8;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 操作区域顶部装饰线 */
|
||||
.info-panel-actions::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #1677ff 0%, transparent 50%, #1677ff 100%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { Row, Col, Space, Button } from 'antd'
|
||||
import './InfoPanel.css'
|
||||
|
||||
/**
|
||||
* 信息展示面板组件
|
||||
* @param {Object} props
|
||||
* @param {Object} props.data - 数据源
|
||||
* @param {Array} props.fields - 字段配置数组
|
||||
* @param {Array} props.actions - 操作按钮配置(可选)
|
||||
* @param {Array} props.gutter - Grid间距配置
|
||||
*/
|
||||
function InfoPanel({ data, fields = [], actions = [], gutter = [24, 16] }) {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<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
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
.list-action-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.list-action-bar-left,
|
||||
.list-action-bar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 搜索和筛选组合 */
|
||||
.list-action-bar-right :global(.ant-space-compact) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.list-action-bar-right :global(.ant-space-compact .ant-input-search) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.list-action-bar-right :global(.ant-space-compact > .ant-btn) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.list-action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.list-action-bar-left,
|
||||
.list-action-bar-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { Button, Input, Space, Popover } from 'antd'
|
||||
import { ReloadOutlined, FilterOutlined } from '@ant-design/icons'
|
||||
import './ListActionBar.css'
|
||||
|
||||
const { Search } = Input
|
||||
|
||||
/**
|
||||
* 列表操作栏组件
|
||||
* @param {Object} props
|
||||
* @param {Array} props.actions - 左侧操作按钮配置数组
|
||||
* @param {Object} props.search - 搜索配置
|
||||
* @param {Object} props.filter - 高级筛选配置(可选)
|
||||
* @param {boolean} props.showRefresh - 是否显示刷新按钮
|
||||
* @param {Function} props.onRefresh - 刷新回调
|
||||
*/
|
||||
function ListActionBar({
|
||||
actions = [],
|
||||
search,
|
||||
filter,
|
||||
showRefresh = false,
|
||||
onRefresh,
|
||||
}) {
|
||||
return (
|
||||
<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
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/* 列表表格容器 */
|
||||
.list-table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 表格行样式 */
|
||||
.list-table-container :global(.ant-table-row) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.list-table-container :global(.ant-table-row:hover) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.list-table-container :global(.ant-table-row.row-selected) {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 操作列样式 - 重新设计 */
|
||||
.list-table-container :global(.ant-table-thead > tr > th:last-child) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.list-table-container :global(.ant-table-tbody > tr > td:last-child) {
|
||||
background: #f8f9ff !important;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.list-table-container :global(.ant-table-tbody > tr:hover > td:last-child) {
|
||||
background: #eef0ff !important;
|
||||
}
|
||||
|
||||
.list-table-container :global(.ant-table-tbody > tr.row-selected > td:last-child) {
|
||||
background: #e1e6ff !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { Table } from 'antd'
|
||||
import './ListTable.css'
|
||||
|
||||
/**
|
||||
* 列表表格组件
|
||||
* @param {Object} props
|
||||
* @param {Array} props.columns - 表格列配置
|
||||
* @param {Array} props.dataSource - 数据源
|
||||
* @param {string} props.rowKey - 行唯一标识字段
|
||||
* @param {Array} props.selectedRowKeys - 选中的行
|
||||
* @param {Function} props.onSelectionChange - 选择变化回调
|
||||
* @param {Object} props.pagination - 分页配置
|
||||
* @param {Object} props.scroll - 表格滚动配置
|
||||
* @param {Function} props.onRowClick - 行点击回调
|
||||
* @param {Object} props.selectedRow - 当前选中的行
|
||||
* @param {boolean} props.loading - 加载状态
|
||||
* @param {string} props.className - 自定义类名
|
||||
*/
|
||||
function ListTable({
|
||||
columns,
|
||||
dataSource,
|
||||
rowKey = 'id',
|
||||
selectedRowKeys = [],
|
||||
onSelectionChange,
|
||||
pagination = {
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
},
|
||||
scroll = { x: 1200 },
|
||||
onRowClick,
|
||||
selectedRow,
|
||||
loading = false,
|
||||
className = '',
|
||||
}) {
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys) => {
|
||||
onSelectionChange?.(newSelectedRowKeys)
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<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,4 +1,5 @@
|
|||
import { Layout, Input, Badge, Avatar, Dropdown, Space } from 'antd'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
|
|
@ -23,6 +24,8 @@ const iconMap = {
|
|||
}
|
||||
|
||||
function AppHeader({ collapsed, onToggle }) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// 用户下拉菜单
|
||||
const userMenuItems = [
|
||||
{
|
||||
|
|
@ -50,6 +53,10 @@ function AppHeader({ collapsed, onToggle }) {
|
|||
|
||||
const handleHeaderMenuClick = (key) => {
|
||||
console.log('Header menu clicked:', key)
|
||||
// 如果点击的是设计文档,跳转到文档页面
|
||||
if (key === 'docs') {
|
||||
navigate('/design')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/* 树形筛选面板 */
|
||||
.tree-filter-panel {
|
||||
width: 320px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 已选择的筛选条件 */
|
||||
.tree-filter-selected {
|
||||
min-height: 40px;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
.tree-filter-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tree-filter-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tree-filter-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.tree-filter-placeholder span {
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 树形选择器容器 */
|
||||
.tree-filter-container {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tree-filter-header {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.tree-filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import { Tree, Tag, Divider, Button, Space } from 'antd'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './TreeFilterPanel.css'
|
||||
|
||||
/**
|
||||
* 树形筛选面板组件
|
||||
* @param {Object} props
|
||||
* @param {Array} props.treeData - 树形数据
|
||||
* @param {string} props.selectedKey - 当前选中的节点ID
|
||||
* @param {string} props.tempSelectedKey - 临时选中的节点ID(确认前)
|
||||
* @param {string} props.treeTitle - 树标题
|
||||
* @param {Function} props.onSelect - 选择变化回调
|
||||
* @param {Function} props.onConfirm - 确认筛选
|
||||
* @param {Function} props.onClear - 清除筛选
|
||||
* @param {string} props.placeholder - 占位提示文本
|
||||
*/
|
||||
function TreeFilterPanel({
|
||||
treeData,
|
||||
selectedKey,
|
||||
tempSelectedKey,
|
||||
treeTitle = '分组筛选',
|
||||
onSelect,
|
||||
onConfirm,
|
||||
onClear,
|
||||
placeholder = '请选择分组进行筛选',
|
||||
}) {
|
||||
// 获取所有节点的key用于默认展开
|
||||
const getAllKeys = (nodes) => {
|
||||
let keys = []
|
||||
const traverse = (node) => {
|
||||
keys.push(node.key)
|
||||
if (node.children) {
|
||||
node.children.forEach(traverse)
|
||||
}
|
||||
}
|
||||
nodes.forEach(traverse)
|
||||
return keys
|
||||
}
|
||||
|
||||
const [expandedKeys, setExpandedKeys] = useState([])
|
||||
|
||||
// 初始化时展开所有节点
|
||||
useEffect(() => {
|
||||
if (treeData && treeData.length > 0) {
|
||||
setExpandedKeys(getAllKeys(treeData))
|
||||
}
|
||||
}, [treeData])
|
||||
|
||||
// 查找节点名称
|
||||
const findNodeName = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.key === id) return node.title
|
||||
if (node.children) {
|
||||
const found = findNodeName(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const handleTreeSelect = (selectedKeys) => {
|
||||
const key = selectedKeys[0] || null
|
||||
onSelect?.(key)
|
||||
}
|
||||
|
||||
const handleExpand = (keys) => {
|
||||
setExpandedKeys(keys)
|
||||
}
|
||||
|
||||
return (
|
||||
<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,75 @@
|
|||
[
|
||||
{
|
||||
"key": "design",
|
||||
"label": "设计规范",
|
||||
"children": [
|
||||
{
|
||||
"key": "design-cookbook",
|
||||
"label": "设计手册",
|
||||
"path": "/docs/DESIGN_COOKBOOK.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "components",
|
||||
"label": "组件文档",
|
||||
"children": [
|
||||
{
|
||||
"key": "components-overview",
|
||||
"label": "组件概览",
|
||||
"path": "/docs/components/README.md"
|
||||
},
|
||||
{
|
||||
"key": "page-title-bar",
|
||||
"label": "PageTitleBar",
|
||||
"path": "/docs/components/PageTitleBar.md"
|
||||
},
|
||||
{
|
||||
"key": "list-action-bar",
|
||||
"label": "ListActionBar",
|
||||
"path": "/docs/components/ListActionBar.md"
|
||||
},
|
||||
{
|
||||
"key": "tree-filter-panel",
|
||||
"label": "TreeFilterPanel",
|
||||
"path": "/docs/components/TreeFilterPanel.md"
|
||||
},
|
||||
{
|
||||
"key": "list-table",
|
||||
"label": "ListTable",
|
||||
"path": "/docs/components/ListTable.md"
|
||||
},
|
||||
{
|
||||
"key": "detail-drawer",
|
||||
"label": "DetailDrawer",
|
||||
"path": "/docs/components/DetailDrawer.md"
|
||||
},
|
||||
{
|
||||
"key": "info-panel",
|
||||
"label": "InfoPanel",
|
||||
"path": "/docs/components/InfoPanel.md"
|
||||
},
|
||||
{
|
||||
"key": "confirm-dialog",
|
||||
"label": "ConfirmDialog",
|
||||
"path": "/docs/components/ConfirmDialog.md"
|
||||
},
|
||||
{
|
||||
"key": "toast",
|
||||
"label": "Toast",
|
||||
"path": "/docs/components/Toast.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pages",
|
||||
"label": "页面文档",
|
||||
"children": [
|
||||
{
|
||||
"key": "main-layout",
|
||||
"label": "主布局",
|
||||
"path": "/docs/pages/main-layout.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
[
|
||||
{
|
||||
"type": "link",
|
||||
"label": "帮助",
|
||||
"label": "使用帮助",
|
||||
"key": "help",
|
||||
"icon": "QuestionCircleOutlined"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"label": "文档",
|
||||
"label": "设计文档",
|
||||
"key": "docs",
|
||||
"icon": "FileTextOutlined"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,15 +2,48 @@
|
|||
"userGroups": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "默认组织",
|
||||
"name": "全部用户",
|
||||
"children": [
|
||||
{
|
||||
"id": "1-1",
|
||||
"name": "黑名单"
|
||||
"name": "管理员组",
|
||||
"children": [
|
||||
{ "id": "1-1-1", "name": "系统管理员" },
|
||||
{ "id": "1-1-2", "name": "安全管理员" },
|
||||
{ "id": "1-1-3", "name": "审计管理员" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1-2",
|
||||
"name": "用户测试分组"
|
||||
"name": "部门用户",
|
||||
"children": [
|
||||
{ "id": "1-2-1", "name": "研发部" },
|
||||
{ "id": "1-2-2", "name": "产品部" },
|
||||
{ "id": "1-2-3", "name": "运营部" },
|
||||
{ "id": "1-2-4", "name": "市场部" },
|
||||
{ "id": "1-2-5", "name": "人力资源部" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1-3",
|
||||
"name": "外部用户",
|
||||
"children": [
|
||||
{ "id": "1-3-1", "name": "供应商" },
|
||||
{ "id": "1-3-2", "name": "合作伙伴" },
|
||||
{ "id": "1-3-3", "name": "临时访客" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1-4",
|
||||
"name": "测试用户",
|
||||
"children": [
|
||||
{ "id": "1-4-1", "name": "功能测试" },
|
||||
{ "id": "1-4-2", "name": "性能测试" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1-5",
|
||||
"name": "黑名单"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -19,10 +52,10 @@
|
|||
{
|
||||
"id": 1,
|
||||
"userName": "admin",
|
||||
"userType": "个人用户",
|
||||
"name": "admin",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"userType": "管理员",
|
||||
"name": "系统管理员",
|
||||
"groupId": "1-1-1",
|
||||
"group": "系统管理员",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 21,
|
||||
|
|
@ -77,10 +110,10 @@
|
|||
"id": 2,
|
||||
"userName": "user",
|
||||
"userType": "个人用户",
|
||||
"name": "user",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"name": "张三",
|
||||
"groupId": "1-2-1",
|
||||
"group": "研发部",
|
||||
"status": "disabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 9,
|
||||
"terminals": [],
|
||||
|
|
@ -90,9 +123,9 @@
|
|||
"id": 3,
|
||||
"userName": "test_user",
|
||||
"userType": "个人用户",
|
||||
"name": "测试用户",
|
||||
"groupId": "1-2",
|
||||
"group": "用户测试分组",
|
||||
"name": "李四",
|
||||
"groupId": "1-4-1",
|
||||
"group": "功能测试",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 8,
|
||||
|
|
@ -102,10 +135,10 @@
|
|||
{
|
||||
"id": 4,
|
||||
"userName": "nex",
|
||||
"userType": "游客",
|
||||
"name": "匿名用户",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"userType": "个人用户",
|
||||
"name": "王五",
|
||||
"groupId": "1-2-2",
|
||||
"group": "产品部",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 10,
|
||||
|
|
@ -116,9 +149,9 @@
|
|||
"id": 5,
|
||||
"userName": "unis",
|
||||
"userType": "个人用户",
|
||||
"name": "unis",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"name": "赵六",
|
||||
"groupId": "1-2-3",
|
||||
"group": "运营部",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 10,
|
||||
|
|
@ -129,9 +162,9 @@
|
|||
"id": 6,
|
||||
"userName": "ceshi123",
|
||||
"userType": "个人用户",
|
||||
"name": "加密测试",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"name": "孙七",
|
||||
"groupId": "1-2-4",
|
||||
"group": "市场部",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 6,
|
||||
"grantedImages": 9,
|
||||
|
|
@ -142,10 +175,10 @@
|
|||
"id": 7,
|
||||
"userName": "ceshi12345",
|
||||
"userType": "个人用户",
|
||||
"name": "测试1",
|
||||
"groupId": "1-1",
|
||||
"name": "黑名单用户",
|
||||
"groupId": "1-5",
|
||||
"group": "黑名单",
|
||||
"status": "enabled",
|
||||
"status": "disabled",
|
||||
"grantedTerminals": 1,
|
||||
"grantedImages": 4,
|
||||
"terminals": [],
|
||||
|
|
@ -154,10 +187,10 @@
|
|||
{
|
||||
"id": 8,
|
||||
"userName": "unis1",
|
||||
"userType": "个人用户",
|
||||
"name": "unis1",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"userType": "管理员",
|
||||
"name": "安全管理员",
|
||||
"groupId": "1-1-2",
|
||||
"group": "安全管理员",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 5,
|
||||
"grantedImages": 6,
|
||||
|
|
@ -167,10 +200,10 @@
|
|||
{
|
||||
"id": 9,
|
||||
"userName": "abcde",
|
||||
"userType": "个人用户",
|
||||
"name": "测试",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"userType": "外部用户",
|
||||
"name": "供应商代表",
|
||||
"groupId": "1-3-1",
|
||||
"group": "供应商",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 0,
|
||||
"grantedImages": 0,
|
||||
|
|
@ -180,10 +213,10 @@
|
|||
{
|
||||
"id": 10,
|
||||
"userName": "qazwsx",
|
||||
"userType": "个人用户",
|
||||
"name": "qazwsx",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"userType": "外部用户",
|
||||
"name": "临时访客",
|
||||
"groupId": "1-3-3",
|
||||
"group": "临时访客",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 0,
|
||||
"grantedImages": 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
/* 文档页面容器 */
|
||||
.docs-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docs-layout {
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 左侧边栏 */
|
||||
.docs-sider {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.docs-sider-header {
|
||||
padding: 24px 24px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.docs-sider-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.docs-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.docs-menu :global(.ant-menu-item) {
|
||||
margin: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.docs-menu :global(.ant-menu-submenu-title) {
|
||||
margin: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.docs-content {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.docs-content-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 60px;
|
||||
}
|
||||
|
||||
.docs-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* Markdown 内容样式 */
|
||||
.markdown-body {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.markdown-body h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
margin-top: 48px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.markdown-body h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.markdown-body p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.markdown-body pre {
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.markdown-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown-body table thead {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border: 1px solid #f0f0f0;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.markdown-body table td {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.markdown-body table tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 引用块 */
|
||||
.markdown-body blockquote {
|
||||
margin: 16px 0;
|
||||
padding: 8px 16px;
|
||||
border-left: 4px solid #1677ff;
|
||||
background: #f0f7ff;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.markdown-body a {
|
||||
color: #1677ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.markdown-body hr {
|
||||
margin: 32px 0;
|
||||
border: none;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
margin: 16px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.docs-sider::-webkit-scrollbar,
|
||||
.docs-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.docs-sider::-webkit-scrollbar-thumb,
|
||||
.docs-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.docs-sider::-webkit-scrollbar-thumb:hover,
|
||||
.docs-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Layout, Menu, Spin } from 'antd'
|
||||
import { FileTextOutlined } from '@ant-design/icons'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import './DocsPage.css'
|
||||
|
||||
const { Sider, Content } = Layout
|
||||
|
||||
// 文档目录数据
|
||||
const docsMenuData = [
|
||||
{
|
||||
key: 'design',
|
||||
label: '设计规范',
|
||||
children: [
|
||||
{
|
||||
key: 'design-cookbook',
|
||||
label: '设计手册',
|
||||
path: '/docs/DESIGN_COOKBOOK.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'components',
|
||||
label: '组件文档',
|
||||
children: [
|
||||
{
|
||||
key: 'components-overview',
|
||||
label: '组件概览',
|
||||
path: '/docs/components/README.md',
|
||||
},
|
||||
{
|
||||
key: 'page-title-bar',
|
||||
label: 'PageTitleBar',
|
||||
path: '/docs/components/PageTitleBar.md',
|
||||
},
|
||||
{
|
||||
key: 'list-action-bar',
|
||||
label: 'ListActionBar',
|
||||
path: '/docs/components/ListActionBar.md',
|
||||
},
|
||||
{
|
||||
key: 'tree-filter-panel',
|
||||
label: 'TreeFilterPanel',
|
||||
path: '/docs/components/TreeFilterPanel.md',
|
||||
},
|
||||
{
|
||||
key: 'list-table',
|
||||
label: 'ListTable',
|
||||
path: '/docs/components/ListTable.md',
|
||||
},
|
||||
{
|
||||
key: 'detail-drawer',
|
||||
label: 'DetailDrawer',
|
||||
path: '/docs/components/DetailDrawer.md',
|
||||
},
|
||||
{
|
||||
key: 'info-panel',
|
||||
label: 'InfoPanel',
|
||||
path: '/docs/components/InfoPanel.md',
|
||||
},
|
||||
{
|
||||
key: 'confirm-dialog',
|
||||
label: 'ConfirmDialog',
|
||||
path: '/docs/components/ConfirmDialog.md',
|
||||
},
|
||||
{
|
||||
key: 'toast',
|
||||
label: 'Toast',
|
||||
path: '/docs/components/Toast.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'pages',
|
||||
label: '页面文档',
|
||||
children: [
|
||||
{
|
||||
key: 'main-layout',
|
||||
label: '主布局',
|
||||
path: '/docs/pages/main-layout.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function DocsPage() {
|
||||
const [selectedKey, setSelectedKey] = useState('design-cookbook')
|
||||
const [markdownContent, setMarkdownContent] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 构建菜单项
|
||||
const menuItems = docsMenuData.map((group) => ({
|
||||
key: group.key,
|
||||
label: group.label,
|
||||
icon: <FileTextOutlined />,
|
||||
children: group.children.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
})),
|
||||
}))
|
||||
|
||||
// 根据 key 查找文档路径
|
||||
const findDocPath = (key) => {
|
||||
for (const group of docsMenuData) {
|
||||
const found = group.children.find((item) => item.key === key)
|
||||
if (found) return found.path
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 加载 markdown 文件
|
||||
const loadMarkdown = async (key) => {
|
||||
const path = findDocPath(key)
|
||||
if (!path) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(path)
|
||||
if (response.ok) {
|
||||
const text = await response.text()
|
||||
setMarkdownContent(text)
|
||||
} else {
|
||||
setMarkdownContent('# 文档加载失败\n\n无法加载该文档,请稍后重试。')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading markdown:', error)
|
||||
setMarkdownContent('# 文档加载失败\n\n' + error.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = ({ key }) => {
|
||||
setSelectedKey(key)
|
||||
loadMarkdown(key)
|
||||
}
|
||||
|
||||
// 初始加载默认文档
|
||||
useEffect(() => {
|
||||
loadMarkdown(selectedKey)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="docs-page">
|
||||
<Layout className="docs-layout">
|
||||
{/* 左侧目录 */}
|
||||
<Sider width={280} className="docs-sider" theme="light">
|
||||
<div className="docs-sider-header">
|
||||
<h2>文档中心</h2>
|
||||
</div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[selectedKey]}
|
||||
defaultOpenKeys={['design', 'components', 'pages']}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
className="docs-menu"
|
||||
/>
|
||||
</Sider>
|
||||
|
||||
{/* 右侧内容 */}
|
||||
<Content className="docs-content">
|
||||
<div className="docs-content-wrapper">
|
||||
{loading ? (
|
||||
<div className="docs-loading">
|
||||
<Spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
) : (
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw, rehypeHighlight]}
|
||||
>
|
||||
{markdownContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocsPage
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* 统计面板 */
|
||||
/* 统计面板 - HostListPage 特有 */
|
||||
.stats-panel {
|
||||
margin-bottom: 16px;
|
||||
animation: slideDown 0.3s ease;
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
box-shadow: 0 6px 20px rgba(22, 119, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 卡片变暗状态(非激活状态下其他卡片) */
|
||||
/* 卡片变暗状态 */
|
||||
.stat-card-dimmed {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.3);
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 筛选结果卡片(不可点击) */
|
||||
/* 筛选结果卡片 */
|
||||
.stat-card-result {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
|
@ -75,330 +75,13 @@
|
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.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);
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 搜索和筛选组合 - 使用 Space.Compact */
|
||||
.action-bar-right :global(.ant-space-compact) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact .ant-input-search) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact > .ant-btn) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* 筛选弹出框内容 */
|
||||
.filter-popover-content {
|
||||
width: 320px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .selected-filters {
|
||||
min-height: 40px;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.filter-popover-content .group-tree-container {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .tree-header {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 弹出框样式 */
|
||||
:global(.filter-popover .ant-popover-inner) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-title) {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-inner-content) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 数据统计 */
|
||||
.data-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card-small {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card-small:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-title) {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 主机名样式 */
|
||||
.host-name {
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-row) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-row:hover) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-row.row-selected) {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 操作列样式优化 - 重新设计 */
|
||||
.host-list-page :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;
|
||||
}
|
||||
|
||||
.host-list-page :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);
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-tbody > tr:hover > td:last-child) {
|
||||
background: #eef0ff !important;
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-tbody > tr.row-selected > td:last-child) {
|
||||
background: #e1e6ff !important;
|
||||
}
|
||||
|
||||
/* 详情抽屉 */
|
||||
.detail-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部信息区域 - 固定不滚动 */
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.host-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.detail-header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 可滚动内容区域 */
|
||||
.detail-scrollable-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 主机信息面板 */
|
||||
.detail-info-panel {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 操作按钮区 */
|
||||
.detail-actions {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 标签页区域 */
|
||||
.detail-tabs {
|
||||
background: #ffffff;
|
||||
padding-top: 16px;
|
||||
padding-left: 12px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-content-holder) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav) {
|
||||
padding: 0;
|
||||
margin: 0 24px;
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav::before) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab) {
|
||||
padding: 12px 0;
|
||||
margin: 0 32px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab:first-child) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab-active .ant-tabs-tab-btn) {
|
||||
color: #d946ef;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-ink-bar) {
|
||||
background: #d946ef;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 卡片列表 */
|
||||
/* 卡片列表 - 详情抽屉中的镜像和用户卡片 */
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -416,6 +99,17 @@
|
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 用户卡片 */
|
||||
.user-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -445,17 +139,7 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
/* 用户卡片 */
|
||||
.user-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 用户卡片特有样式 */
|
||||
.user-header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -493,69 +177,6 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
/* 旧版详情抽屉样式(保留用于编辑抽屉) */
|
||||
.detail-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-header h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-header :global(.ant-badge-status-text) {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-actions button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1200px) {
|
||||
.stats-panel :global(.ant-row) {
|
||||
|
|
@ -565,17 +186,6 @@
|
|||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-panel :global(.ant-row) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,165 +1,3 @@
|
|||
.image-list-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.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);
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-row) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-row:hover) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-row.row-selected) {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 操作列样式优化 - 重新设计 */
|
||||
.image-list-page :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;
|
||||
}
|
||||
|
||||
.image-list-page :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);
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-tbody > tr:hover > td:last-child) {
|
||||
background: #eef0ff !important;
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-tbody > tr.row-selected > td:last-child) {
|
||||
background: #e1e6ff !important;
|
||||
}
|
||||
|
||||
/* 详情抽屉 */
|
||||
.detail-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部信息区域 - 固定不滚动 */
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.image-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.detail-header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 可滚动内容区域 */
|
||||
.detail-scrollable-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 镜像信息面板 */
|
||||
.detail-info-panel {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,21 @@
|
|||
import { useState } from 'react'
|
||||
import { Button, Space } from 'antd'
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Space,
|
||||
Drawer,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd'
|
||||
import {
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
DownloadOutlined,
|
||||
EyeOutlined,
|
||||
DeleteOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
||||
import ListTable from '../components/ListTable/ListTable'
|
||||
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||
import Toast from '../components/Toast/Toast'
|
||||
import imageData from '../data/imageData.json'
|
||||
import './ImageListPage.css'
|
||||
|
||||
const { Search } = Input
|
||||
|
||||
function ImageListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
const [selectedImage, setSelectedImage] = useState(null)
|
||||
|
|
@ -97,13 +89,15 @@ function ImageListPage() {
|
|||
},
|
||||
]
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys)
|
||||
},
|
||||
}
|
||||
// 字段配置 - 用于InfoPanel
|
||||
const imageFields = [
|
||||
{ key: 'name', label: '镜像名称', span: 12 },
|
||||
{ key: 'fileName', label: '镜像文件', span: 12 },
|
||||
{ key: 'version', label: '镜像版本', span: 12 },
|
||||
{ key: 'os', label: '操作系统', span: 12 },
|
||||
{ key: 'filePath', label: '镜像存放路径', span: 24 },
|
||||
{ key: 'btPath', label: 'BT路径', span: 24 },
|
||||
]
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (value) => {
|
||||
|
|
@ -185,157 +179,77 @@ function ImageListPage() {
|
|||
showToggle={false}
|
||||
/>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="action-bar">
|
||||
<div className="action-bar-left">
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
新建
|
||||
</Button>
|
||||
<Button icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} danger onClick={handleBatchDelete}>
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
<div className="action-bar-right">
|
||||
<Search
|
||||
placeholder="请输入名称"
|
||||
allowClear
|
||||
style={{ width: 280 }}
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
value={searchKeyword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 操作栏 - 使用新组件 */}
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新建',
|
||||
icon: <PlusOutlined />,
|
||||
type: 'primary',
|
||||
onClick: () => console.log('新建')
|
||||
},
|
||||
{
|
||||
key: 'batchDelete',
|
||||
label: '批量删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
disabled: selectedRowKeys.length === 0,
|
||||
onClick: handleBatchDelete
|
||||
},
|
||||
]}
|
||||
search={{
|
||||
placeholder: '请输入名称',
|
||||
value: searchKeyword,
|
||||
onSearch: handleSearch,
|
||||
onChange: handleSearch,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className="table-container">
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredImages}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
total: filteredImages.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleRowClick(record),
|
||||
className: selectedImage?.id === record.id ? 'row-selected' : '',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* 数据表格 - 使用新组件 */}
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={filteredImages}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onSelectionChange={setSelectedRowKeys}
|
||||
onRowClick={handleRowClick}
|
||||
selectedRow={selectedImage}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
|
||||
{/* 详情抽屉 */}
|
||||
<Drawer
|
||||
title={null}
|
||||
placement="right"
|
||||
width={1080}
|
||||
{/* 详情抽屉 - 使用新组件 */}
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
open={showDetailDrawer}
|
||||
closable={false}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
title={{ text: selectedImage?.name || '' }}
|
||||
headerActions={[
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
setShowDetailDrawer(false)
|
||||
if (selectedImage) handleDeleteImage(selectedImage)
|
||||
},
|
||||
},
|
||||
]}
|
||||
width={1080}
|
||||
>
|
||||
{selectedImage && (
|
||||
<div className="detail-drawer-content">
|
||||
{/* 顶部标题栏 */}
|
||||
<div className="detail-header">
|
||||
<div className="detail-header-left">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => setShowDetailDrawer(false)}
|
||||
className="close-button"
|
||||
/>
|
||||
<div className="header-info">
|
||||
<h2 className="image-title">{selectedImage.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-header-right">
|
||||
<Space size="middle">
|
||||
<Button icon={<DownloadOutlined />}>下载</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => {
|
||||
setShowDetailDrawer(false)
|
||||
handleDeleteImage(selectedImage)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可滚动内容区域 */}
|
||||
<div className="detail-scrollable-content">
|
||||
{/* 镜像信息面板 */}
|
||||
<div className="detail-info-panel">
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像名称:</div>
|
||||
<div className="info-value">{selectedImage.name}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像文件:</div>
|
||||
<div className="info-value">{selectedImage.fileName}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像版本:</div>
|
||||
<div className="info-value">{selectedImage.version}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">操作系统:</div>
|
||||
<div className="info-value">{selectedImage.os}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像状态:</div>
|
||||
<div className="info-value">{selectedImage.status}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">上传时间:</div>
|
||||
<div className="info-value">{selectedImage.uploadTime}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像存放路径:</div>
|
||||
<div className="info-value">{selectedImage.filePath}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">BT路径:</div>
|
||||
<div className="info-value">{selectedImage.btPath}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">描述:</div>
|
||||
<div className="info-value">{selectedImage.description}</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
<InfoPanel
|
||||
data={selectedImage}
|
||||
fields={imageFields}
|
||||
actions={[
|
||||
{
|
||||
key: 'download',
|
||||
label: '下载镜像',
|
||||
icon: <DownloadOutlined />,
|
||||
type: 'primary',
|
||||
onClick: () => console.log('下载镜像'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DetailDrawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* 统计面板 */
|
||||
/* 统计面板 - UserListPage 特有 */
|
||||
.stats-panel {
|
||||
margin-bottom: 16px;
|
||||
animation: slideDown 0.3s ease;
|
||||
|
|
@ -75,301 +75,13 @@
|
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.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);
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 搜索和筛选组合 */
|
||||
.action-bar-right :global(.ant-space-compact) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact .ant-input-search) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact > .ant-btn) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* 筛选弹出框内容 */
|
||||
.filter-popover-content {
|
||||
width: 320px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .selected-filters {
|
||||
min-height: 40px;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.filter-popover-content .group-tree-container {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .tree-header {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 弹出框样式 */
|
||||
:global(.filter-popover .ant-popover-inner) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-title) {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-inner-content) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 用户名样式 */
|
||||
.user-name-text {
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-row) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-row:hover) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-row.row-selected) {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 操作列样式优化 - 重新设计 */
|
||||
.user-list-page :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;
|
||||
}
|
||||
|
||||
.user-list-page :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);
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-tbody > tr:hover > td:last-child) {
|
||||
background: #eef0ff !important;
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-tbody > tr.row-selected > td:last-child) {
|
||||
background: #e1e6ff !important;
|
||||
}
|
||||
|
||||
/* 详情抽屉 */
|
||||
.detail-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部信息区域 - 固定不滚动 */
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.detail-header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 可滚动内容区域 */
|
||||
.detail-scrollable-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 用户信息面板 */
|
||||
.detail-info-panel {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 操作按钮区 */
|
||||
.detail-actions {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 标签页区域 */
|
||||
.detail-tabs {
|
||||
background: #ffffff;
|
||||
padding-top: 16px;
|
||||
padding-left: 12px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-content-holder) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav) {
|
||||
padding: 0;
|
||||
margin: 0 24px;
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav::before) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab) {
|
||||
padding: 12px 0;
|
||||
margin: 0 32px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab:first-child) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab-active .ant-tabs-tab-btn) {
|
||||
color: #d946ef;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-ink-bar) {
|
||||
background: #d946ef;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 卡片列表 */
|
||||
/* 卡片列表 - 详情抽屉中的终端和镜像卡片 */
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -438,17 +150,6 @@
|
|||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-panel :global(.ant-row) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,45 @@
|
|||
import { useState } from 'react'
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Tag,
|
||||
Badge,
|
||||
Drawer,
|
||||
Tree,
|
||||
Dropdown,
|
||||
Space,
|
||||
Form,
|
||||
Select,
|
||||
Input,
|
||||
Divider,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Popover,
|
||||
Switch,
|
||||
Tabs,
|
||||
Badge,
|
||||
Drawer,
|
||||
TreeSelect,
|
||||
} from 'antd'
|
||||
import {
|
||||
UserOutlined,
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
MoreOutlined,
|
||||
CloseOutlined,
|
||||
LockOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
LockOutlined,
|
||||
DesktopOutlined,
|
||||
DatabaseOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||||
import TreeFilterPanel from '../components/TreeFilterPanel/TreeFilterPanel'
|
||||
import ListTable from '../components/ListTable/ListTable'
|
||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
||||
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||
import Toast from '../components/Toast/Toast'
|
||||
import userData from '../data/userData.json'
|
||||
import './UserListPage.css'
|
||||
|
||||
const { Search } = Input
|
||||
|
||||
function UserListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
const [selectedUser, setSelectedUser] = useState(null)
|
||||
|
|
@ -150,55 +146,26 @@ function UserListPage() {
|
|||
},
|
||||
]
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys)
|
||||
// 用户信息字段配置
|
||||
const userFields = [
|
||||
{ key: 'userName', label: '用户名', span: 6 },
|
||||
{ key: 'group', label: '用户分组', span: 6 },
|
||||
{ key: 'name', label: '姓名', span: 6 },
|
||||
{ key: 'grantedImages', label: '授权镜像', span: 6 },
|
||||
{ key: 'userType', label: '用户类型', span: 6 },
|
||||
{ key: 'grantedTerminals', label: '授权终端', span: 6 },
|
||||
{
|
||||
key: 'status',
|
||||
label: '启停用',
|
||||
span: 6,
|
||||
render: (value) => (
|
||||
<Tag color={value === 'enabled' ? 'green' : 'default'}>
|
||||
{value === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (value) => {
|
||||
setSearchKeyword(value)
|
||||
filterUsers(value, selectedGroup, statusFilter)
|
||||
}
|
||||
|
||||
// 处理分组选择
|
||||
const handleGroupSelect = (selectedKeys, info) => {
|
||||
const groupId = selectedKeys[0]
|
||||
setTempSelectedGroup(groupId)
|
||||
}
|
||||
|
||||
// 确认筛选
|
||||
const handleConfirmFilter = () => {
|
||||
setSelectedGroup(tempSelectedGroup)
|
||||
const findGroupName = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node.name
|
||||
if (node.children) {
|
||||
const found = findGroupName(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const groupName = tempSelectedGroup
|
||||
? findGroupName(userData.userGroups, tempSelectedGroup)
|
||||
: ''
|
||||
setSelectedGroupName(groupName)
|
||||
filterUsers(searchKeyword, tempSelectedGroup, statusFilter)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
|
||||
// 清除筛选
|
||||
const handleClearFilter = () => {
|
||||
setTempSelectedGroup(null)
|
||||
setSelectedGroup(null)
|
||||
setSelectedGroupName('')
|
||||
filterUsers(searchKeyword, null, statusFilter)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
{ key: 'description', label: '描述', span: 18, render: () => '--' },
|
||||
]
|
||||
|
||||
// 过滤用户
|
||||
const filterUsers = (keyword, groupId, status = statusFilter) => {
|
||||
|
|
@ -223,6 +190,12 @@ function UserListPage() {
|
|||
setFilteredUsers(filtered)
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (value) => {
|
||||
setSearchKeyword(value)
|
||||
filterUsers(value, selectedGroup, statusFilter)
|
||||
}
|
||||
|
||||
// 处理状态筛选
|
||||
const handleStatusFilterClick = (status) => {
|
||||
const newStatusFilter = statusFilter === status ? null : status
|
||||
|
|
@ -238,6 +211,36 @@ function UserListPage() {
|
|||
}
|
||||
}
|
||||
|
||||
// 树形筛选 - 确认
|
||||
const handleConfirmFilter = () => {
|
||||
setSelectedGroup(tempSelectedGroup)
|
||||
const findGroupName = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node.name
|
||||
if (node.children) {
|
||||
const found = findGroupName(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const groupName = tempSelectedGroup
|
||||
? findGroupName(userData.userGroups, tempSelectedGroup)
|
||||
: ''
|
||||
setSelectedGroupName(groupName)
|
||||
filterUsers(searchKeyword, tempSelectedGroup, statusFilter)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
|
||||
// 树形筛选 - 清除
|
||||
const handleClearFilter = () => {
|
||||
setTempSelectedGroup(null)
|
||||
setSelectedGroup(null)
|
||||
setSelectedGroupName('')
|
||||
filterUsers(searchKeyword, null, statusFilter)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDeleteUser = (record) => {
|
||||
ConfirmDialog.delete({
|
||||
|
|
@ -285,7 +288,7 @@ function UserListPage() {
|
|||
// 处理行点击
|
||||
const handleRowClick = (record) => {
|
||||
setSelectedUser(record)
|
||||
setShowEditDrawer(false) // 关闭编辑抽屉
|
||||
setShowEditDrawer(false)
|
||||
setShowDetailDrawer(true)
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +296,7 @@ function UserListPage() {
|
|||
const handleEditUser = (record) => {
|
||||
setSelectedUser(record)
|
||||
setEditMode('edit')
|
||||
setShowDetailDrawer(false) // 关闭详情抽屉
|
||||
setShowDetailDrawer(false)
|
||||
setShowEditDrawer(true)
|
||||
}
|
||||
|
||||
|
|
@ -308,58 +311,164 @@ function UserListPage() {
|
|||
|
||||
const treeData = convertTreeData(userData.userGroups)
|
||||
|
||||
// 高级筛选面板内容
|
||||
const filterContent = (
|
||||
<div className="filter-popover-content">
|
||||
<div className="selected-filters">
|
||||
{tempSelectedGroup ? (
|
||||
<div className="filter-tag">
|
||||
<span className="filter-label">已选择分组:</span>
|
||||
<Tag color="blue" closable onClose={() => setTempSelectedGroup(null)}>
|
||||
{(() => {
|
||||
const findName = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node.name
|
||||
if (node.children) {
|
||||
const found = findName(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return findName(userData.userGroups, tempSelectedGroup)
|
||||
})()}
|
||||
</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<div className="filter-placeholder">
|
||||
<span style={{ color: '#8c8c8c', fontSize: '13px' }}>请选择用户分组进行筛选</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<div className="group-tree-container">
|
||||
<div className="tree-header">用户分组</div>
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
defaultExpandAll
|
||||
onSelect={handleGroupSelect}
|
||||
selectedKeys={tempSelectedGroup ? [tempSelectedGroup] : []}
|
||||
/>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<div className="filter-actions">
|
||||
<Space>
|
||||
<Button size="small" onClick={handleClearFilter}>
|
||||
清除筛选
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={handleConfirmFilter}>
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
// 详情抽屉的标签页内容
|
||||
const detailTabs = [
|
||||
{
|
||||
key: 'terminals',
|
||||
label: (
|
||||
<span>
|
||||
<DesktopOutlined style={{ marginRight: 8 }} />
|
||||
授权终端
|
||||
</span>
|
||||
),
|
||||
content: (
|
||||
<div className="card-list">
|
||||
{selectedUser?.terminals && selectedUser.terminals.length > 0 ? (
|
||||
selectedUser.terminals.map((terminal, index) => (
|
||||
<Card key={terminal.id} className="terminal-card">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
<DesktopOutlined style={{ marginRight: 8, color: '#1677ff' }} />
|
||||
{terminal.name}
|
||||
</h4>
|
||||
<Badge
|
||||
status={terminal.status === '在线' ? 'success' : 'default'}
|
||||
text={terminal.status}
|
||||
/>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">序号:</span>
|
||||
<span className="card-value">{index + 1}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">终端分组:</span>
|
||||
<span className="card-value">{terminal.group}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">MAC地址:</span>
|
||||
<span className="card-value">{terminal.mac}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">IP地址:</span>
|
||||
<span className="card-value">{terminal.ip}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">授权镜像:</span>
|
||||
<span className="card-value">{terminal.grantedImages}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">数据盘容量:</span>
|
||||
<span className="card-value">{terminal.dataDisk}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">系统盘容量:</span>
|
||||
<span className="card-value">{terminal.systemDisk}</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
移除授权
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: '#8c8c8c', padding: '24px', textAlign: 'center' }}>
|
||||
该用户暂无授权终端
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'images',
|
||||
label: (
|
||||
<span>
|
||||
<DatabaseOutlined style={{ marginRight: 8 }} />
|
||||
授权镜像
|
||||
</span>
|
||||
),
|
||||
content: (
|
||||
<div className="card-list">
|
||||
{selectedUser?.images && selectedUser.images.length > 0 ? (
|
||||
selectedUser.images.map((image) => (
|
||||
<Card key={image.id} className="image-card">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
<DatabaseOutlined style={{ marginRight: 8, color: '#52c41a' }} />
|
||||
{image.name}
|
||||
</h4>
|
||||
<Tag color={image.status === '已下发' ? 'success' : 'default'}>
|
||||
{image.status}
|
||||
</Tag>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">系统镜像:</span>
|
||||
<span className="card-value">{image.system}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">操作系统:</span>
|
||||
<span className="card-value">{image.os}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">创建时间:</span>
|
||||
<span className="card-value">{image.createTime}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">下发方式:</span>
|
||||
<span className="card-value">{image.method}</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
移除授权
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: '#8c8c8c', padding: '24px', textAlign: 'center' }}>
|
||||
该用户暂无授权镜像
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="user-list-page">
|
||||
|
|
@ -434,381 +543,169 @@ function UserListPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="action-bar">
|
||||
<div className="action-bar-left">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
{/* 操作栏 - 使用新组件 */}
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新增用户',
|
||||
icon: <PlusOutlined />,
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
setEditMode('add')
|
||||
setSelectedUser(null)
|
||||
setShowDetailDrawer(false) // 关闭详情抽屉
|
||||
setShowDetailDrawer(false)
|
||||
setShowEditDrawer(true)
|
||||
}}
|
||||
>
|
||||
新增用户
|
||||
</Button>
|
||||
<Button icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} danger onClick={handleBatchDelete}>
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
<div className="action-bar-right">
|
||||
<Space.Compact>
|
||||
<Search
|
||||
placeholder="搜索用户名或姓名"
|
||||
allowClear
|
||||
style={{ width: 280 }}
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
value={searchKeyword}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'batchDelete',
|
||||
label: '批量删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
disabled: selectedRowKeys.length === 0,
|
||||
onClick: handleBatchDelete,
|
||||
},
|
||||
]}
|
||||
search={{
|
||||
placeholder: '搜索用户名或姓名',
|
||||
value: searchKeyword,
|
||||
onSearch: handleSearch,
|
||||
onChange: handleSearch,
|
||||
}}
|
||||
filter={{
|
||||
content: (
|
||||
<TreeFilterPanel
|
||||
treeData={treeData}
|
||||
selectedKey={selectedGroup}
|
||||
tempSelectedKey={tempSelectedGroup}
|
||||
treeTitle="用户分组"
|
||||
onSelect={setTempSelectedGroup}
|
||||
onConfirm={handleConfirmFilter}
|
||||
onClear={handleClearFilter}
|
||||
placeholder="请选择用户分组进行筛选"
|
||||
/>
|
||||
<Popover
|
||||
content={filterContent}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FilterOutlined />
|
||||
<span>高级筛选</span>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
open={showFilterPopover}
|
||||
onOpenChange={(visible) => {
|
||||
setShowFilterPopover(visible)
|
||||
if (visible) {
|
||||
setTempSelectedGroup(selectedGroup)
|
||||
}
|
||||
}}
|
||||
placement="bottomRight"
|
||||
overlayClassName="filter-popover"
|
||||
>
|
||||
<Button icon={<FilterOutlined />} type={selectedGroup ? 'primary' : 'default'}>
|
||||
{selectedGroupName || '筛选'}
|
||||
</Button>
|
||||
</Popover>
|
||||
</Space.Compact>
|
||||
<Button icon={<ReloadOutlined />}>刷新</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
title: '高级筛选',
|
||||
visible: showFilterPopover,
|
||||
onVisibleChange: (visible) => {
|
||||
setShowFilterPopover(visible)
|
||||
if (visible) {
|
||||
setTempSelectedGroup(selectedGroup)
|
||||
}
|
||||
},
|
||||
selectedLabel: selectedGroupName,
|
||||
isActive: !!selectedGroup,
|
||||
}}
|
||||
showRefresh
|
||||
onRefresh={() => console.log('刷新')}
|
||||
/>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className="table-container">
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredUsers}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
total: filteredUsers.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 1400 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleRowClick(record),
|
||||
className: selectedUser?.id === record.id ? 'row-selected' : '',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* 数据表格 - 使用新组件 */}
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={filteredUsers}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onSelectionChange={setSelectedRowKeys}
|
||||
onRowClick={handleRowClick}
|
||||
selectedRow={selectedUser}
|
||||
scroll={{ x: 1400 }}
|
||||
/>
|
||||
|
||||
{/* 详情抽屉 */}
|
||||
<Drawer
|
||||
title={null}
|
||||
placement="right"
|
||||
width={1080}
|
||||
{/* 详情抽屉 - 使用新组件 */}
|
||||
<DetailDrawer
|
||||
visible={showDetailDrawer}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
open={showDetailDrawer}
|
||||
closable={false}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
title={{
|
||||
text: selectedUser?.userName || '',
|
||||
badge: (
|
||||
<Tag color={selectedUser?.status === 'enabled' ? 'green' : 'default'}>
|
||||
{selectedUser?.status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
}}
|
||||
headerActions={[
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setEditMode('edit')
|
||||
setShowEditDrawer(true)
|
||||
setShowDetailDrawer(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
setShowDetailDrawer(false)
|
||||
if (selectedUser) handleDeleteUser(selectedUser)
|
||||
},
|
||||
},
|
||||
]}
|
||||
width={1080}
|
||||
tabs={detailTabs}
|
||||
>
|
||||
{selectedUser && (
|
||||
<div className="detail-drawer-content">
|
||||
{/* 顶部标题栏 - 固定不滚动 */}
|
||||
<div className="detail-header">
|
||||
<div className="detail-header-left">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => setShowDetailDrawer(false)}
|
||||
className="close-button"
|
||||
/>
|
||||
<div className="header-info">
|
||||
<h2 className="user-title">{selectedUser.userName}</h2>
|
||||
<Tag color={selectedUser.status === 'enabled' ? 'green' : 'default'}>
|
||||
{selectedUser.status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-header-right">
|
||||
<Space size="middle">
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setEditMode('edit')
|
||||
setShowEditDrawer(true)
|
||||
setShowDetailDrawer(false)
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => {
|
||||
setShowDetailDrawer(false)
|
||||
handleDeleteUser(selectedUser)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<InfoPanel
|
||||
data={selectedUser}
|
||||
fields={userFields}
|
||||
actions={[
|
||||
{ key: 'move', label: '转移分组', type: 'primary', onClick: () => console.log('转移分组') },
|
||||
{ key: 'blacklist', label: '加入黑名单', onClick: () => console.log('加入黑名单') },
|
||||
{ key: 'reset', label: '重置密码', onClick: () => console.log('重置密码') },
|
||||
{ key: 'disable', label: '停用', icon: <LockOutlined />, onClick: () => console.log('停用') },
|
||||
]}
|
||||
/>
|
||||
</DetailDrawer>
|
||||
|
||||
{/* 可滚动内容区域 */}
|
||||
<div className="detail-scrollable-content">
|
||||
{/* 用户信息面板 */}
|
||||
<div className="detail-info-panel">
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">用户名:</div>
|
||||
<div className="info-value">{selectedUser.userName}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">用户分组:</div>
|
||||
<div className="info-value">{selectedUser.group}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">姓名:</div>
|
||||
<div className="info-value">{selectedUser.name}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">授权镜像:</div>
|
||||
<div className="info-value">{selectedUser.grantedImages}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">用户类型:</div>
|
||||
<div className="info-value">{selectedUser.userType}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">授权终端:</div>
|
||||
<div className="info-value">{selectedUser.grantedTerminals}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">启停用:</div>
|
||||
<div className="info-value">
|
||||
<Tag color={selectedUser.status === 'enabled' ? 'green' : 'default'}>
|
||||
{selectedUser.status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">描述:</div>
|
||||
<div className="info-value">--</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 操作按钮区 */}
|
||||
<div className="detail-actions">
|
||||
<Space size="middle">
|
||||
<Button type="primary">转移分组</Button>
|
||||
<Button>加入黑名单</Button>
|
||||
<Button>重置密码</Button>
|
||||
<Button icon={<LockOutlined />}>停用</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标签页区域 */}
|
||||
<div className="detail-tabs">
|
||||
<Tabs
|
||||
defaultActiveKey="terminals"
|
||||
type="line"
|
||||
size="large"
|
||||
items={[
|
||||
{
|
||||
key: 'terminals',
|
||||
label: (
|
||||
<span>
|
||||
<DesktopOutlined style={{ marginRight: 8 }} />
|
||||
授权终端
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="tab-content">
|
||||
<div className="card-list">
|
||||
{selectedUser.terminals && selectedUser.terminals.length > 0 ? (
|
||||
selectedUser.terminals.map((terminal, index) => (
|
||||
<Card key={terminal.id} className="terminal-card">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
<DesktopOutlined style={{ marginRight: 8, color: '#1677ff' }} />
|
||||
{terminal.name}
|
||||
</h4>
|
||||
<Badge
|
||||
status={terminal.status === '在线' ? 'success' : 'default'}
|
||||
text={terminal.status}
|
||||
/>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">序号:</span>
|
||||
<span className="card-value">{index + 1}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">终端分组:</span>
|
||||
<span className="card-value">{terminal.group}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">MAC地址:</span>
|
||||
<span className="card-value">{terminal.mac}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">IP地址:</span>
|
||||
<span className="card-value">{terminal.ip}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">授权镜像:</span>
|
||||
<span className="card-value">{terminal.grantedImages}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">数据盘容量:</span>
|
||||
<span className="card-value">{terminal.dataDisk}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">系统盘容量:</span>
|
||||
<span className="card-value">{terminal.systemDisk}</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
移除授权
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: '#8c8c8c', padding: '24px', textAlign: 'center' }}>
|
||||
该用户暂无授权终端
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'images',
|
||||
label: (
|
||||
<span>
|
||||
<DatabaseOutlined style={{ marginRight: 8 }} />
|
||||
授权镜像
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="tab-content">
|
||||
<div className="card-list">
|
||||
{selectedUser.images && selectedUser.images.length > 0 ? (
|
||||
selectedUser.images.map((image) => (
|
||||
<Card key={image.id} className="image-card">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
<DatabaseOutlined style={{ marginRight: 8, color: '#52c41a' }} />
|
||||
{image.name}
|
||||
</h4>
|
||||
<Tag color={image.status === '已下发' ? 'success' : 'default'}>
|
||||
{image.status}
|
||||
</Tag>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">系统镜像:</span>
|
||||
<span className="card-value">{image.system}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">操作系统:</span>
|
||||
<span className="card-value">{image.os}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">创建时间:</span>
|
||||
<span className="card-value">{image.createTime}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">下发方式:</span>
|
||||
<span className="card-value">{image.method}</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
移除授权
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: '#8c8c8c', padding: '24px', textAlign: 'center' }}>
|
||||
该用户暂无授权镜像
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 新增/编辑抽屉 */}
|
||||
<Drawer
|
||||
title={
|
||||
<Space>
|
||||
<UserOutlined />
|
||||
<span>{editMode === 'add' ? '新增用户' : '编辑用户'}</span>
|
||||
</Space>
|
||||
}
|
||||
placement="right"
|
||||
width={720}
|
||||
onClose={() => setShowEditDrawer(false)}
|
||||
open={showEditDrawer}
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => setShowEditDrawer(false)}>取消</Button>
|
||||
<Button type="primary">保存</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" initialValues={editMode === 'edit' ? selectedUser : {}}>
|
||||
<Form.Item label="用户名" name="userName" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="姓名" name="name" rules={[{ required: true, message: '请输入姓名' }]}>
|
||||
<Input placeholder="请输入姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="用户分组" name="groupId" rules={[{ required: true, message: '请选择用户分组' }]}>
|
||||
<TreeSelect
|
||||
treeData={treeData}
|
||||
placeholder="请选择用户分组"
|
||||
treeDefaultExpandAll
|
||||
showSearch
|
||||
filterTreeNode={(search, item) =>
|
||||
item.title.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="用户类型" name="userType">
|
||||
<Select placeholder="请选择用户类型">
|
||||
<Select.Option value="管理员">管理员</Select.Option>
|
||||
<Select.Option value="个人用户">个人用户</Select.Option>
|
||||
<Select.Option value="外部用户">外部用户</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue