重定义了几个新组件

main
mula.liu 2025-11-13 18:11:08 +08:00
parent aafc2c6bf0
commit e9691882f0
55 changed files with 5226 additions and 500 deletions

View File

@ -11,6 +11,29 @@
- Nginx serve: 静态文件服务器
- Node.js 18: 运行环境
## 项目构建说明
### 文档目录处理
本项目的 `/design` 路由会加载项目根目录下的 `docs/` 文件夹中的 Markdown 文档。通过 Docker 卷挂载方式,将宿主机的 `docs/` 目录映射到容器内的 `/app/dist/docs/`
**优势:**
- ✅ 实时更新:修改 MD 文件后无需重新构建镜像
- ✅ 方便维护:在宿主机直接编辑文档
- ✅ 轻量镜像Docker 镜像不包含文档,体积更小
- ✅ 灵活部署:可以独立管理文档版本
**配置方式:**
```yaml
# docker-compose.yml
volumes:
- ./docs:/app/dist/docs:ro # :ro 表示只读挂载,提高安全性
```
**注意事项:**
- 部署时需要确保 `docs/` 目录存在于项目根目录
- 如需制作完全自包含的镜像,可以在 Dockerfile 中 COPY docs 目录
## 文件说明
### 1. ecosystem.config.js

155
QUICKSTART.md 100644
View File

@ -0,0 +1,155 @@
# Docker + PM2 部署快速参考
## 🚀 快速开始
```bash
# 本地构建测试
yarn build
yarn preview
# Docker 部署
docker-compose up -d --build
# 查看日志
docker-compose logs -f
```
## 📝 常用命令
### 本地开发
```bash
yarn dev # 启动开发服务器
yarn build # 构建生产版本
yarn preview # 预览构建结果
yarn clean # 清理构建产物
```
### Docker 操作
```bash
# 基础操作
docker-compose up -d # 启动服务
docker-compose down # 停止并删除容器
docker-compose restart # 重启服务
docker-compose logs -f nex-design # 查看日志
# 重新构建
docker-compose up -d --build # 重新构建并启动
docker-compose build --no-cache # 不使用缓存重新构建
# 容器管理
docker exec -it nex-design-app sh # 进入容器
docker ps # 查看运行中的容器
docker stats nex-design-app # 查看资源使用
```
### PM2 管理(容器内)
```bash
pm2 list # 查看进程列表
pm2 logs # 查看日志
pm2 restart nex-design # 重启应用
pm2 monit # 实时监控
```
## 🔧 故障排查
### 文档无法加载
```bash
# 检查 docs 目录是否正确挂载
docker exec nex-design-app ls -la /app/dist/docs/
# 检查挂载配置
docker inspect nex-design-app | grep -A 10 Mounts
# 如果挂载失败,重启容器
docker-compose down
docker-compose up -d
```
### 文档修改后未生效
```bash
# 卷挂载方案下,修改应立即生效
# 如果未生效,检查浏览器缓存
# 使用 Ctrl+Shift+R 强制刷新
# 验证容器内文件已更新
docker exec nex-design-app cat /app/dist/docs/DESIGN_COOKBOOK.md
```
### 容器启动失败
```bash
# 查看详细日志
docker-compose logs nex-design
# 检查端口占用
lsof -i :3000
```
### 内存不足
```bash
# 查看资源使用
docker stats nex-design-app
# 调整 ecosystem.config.js 中的 max_memory_restart
```
## 📁 重要文件
| 文件 | 说明 |
|------|------|
| `ecosystem.config.js` | PM2 配置 |
| `Dockerfile` | Docker 镜像配置 |
| `docker-compose.yml` | Docker Compose 编排 |
| `.dockerignore` | Docker 构建忽略 |
| `DEPLOYMENT.md` | 完整部署文档 |
| `docs/DOCKER_DOCS_SETUP.md` | 文档目录问题说明 |
## 🌐 访问地址
- **本地开发**: http://localhost:5173
- **本地预览**: http://localhost:4173
- **Docker 容器**: http://localhost:3000
## ⚙️ 环境要求
- Node.js >= 16.0.0
- Yarn >= 1.22.0
- Docker >= 20.10
- Docker Compose >= 2.0
## 📦 构建流程
```
源代码
vite build → dist/*
Docker 镜像: dist → /app/dist
卷挂载: ./docs → /app/dist/docs (实时同步)
PM2 serve: /app/dist @ :3000
```
**文档更新流程:**
```
编辑 docs/*.md → 立即生效(无需重启)
```
## 🔐 生产环境
建议配置 Nginx 反向代理和 SSL
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
详见 `DEPLOYMENT.md` 完整配置。

View File

@ -49,6 +49,28 @@ yarn build
yarn preview
```
## 生产部署
### 使用 Docker + PM2 部署
```bash
# 构建并启动容器
docker-compose up -d --build
# 查看运行状态
docker-compose ps
# 查看日志
docker-compose logs -f
```
访问 http://localhost:3000
**部署文档:**
- [快速开始](./QUICKSTART.md) - 快速参考和常用命令
- [完整部署文档](./DEPLOYMENT.md) - 详细的部署配置和优化
- [文档目录配置](./docs/DOCKER_DOCS_SETUP.md) - docs 目录问题说明
## 项目结构
```
@ -127,10 +149,12 @@ Nex Design/
## 版本历史
- **v1.0.0** (2024-11-04)
- **v1.0.0** (2025-11-05)
- 初始化项目
- 创建基础设计规范文档
- 建立项目结构
- 添加 Docker + PM2 部署支持
- 配置完整的部署文档
## 许可证

View File

@ -0,0 +1,178 @@
# Docker 卷挂载方案 - 实施总结
## ✅ 方案优势
相比构建时复制方案,卷挂载方案具有以下优势:
### 🚀 实时更新
- 修改 MD 文档后**立即生效**
- **无需**重新构建 Docker 镜像
- **无需**重启容器
- 开发体验极佳
### 💡 简化流程
- 构建流程更简单,无需 prebuild 脚本
- 镜像更轻量,不包含文档内容
- 文档和代码分离,职责清晰
### 🔄 灵活部署
- 可以独立管理文档版本
- 支持多环境使用不同文档
- 易于回滚和更新
## 📝 实施内容
### 1. 核心配置docker-compose.yml:16
```yaml
volumes:
- ./docs:/app/dist/docs:ro
```
**说明:**
- 将宿主机 `./docs` 挂载到容器 `/app/dist/docs`
- `:ro` 只读挂载,提高安全性
### 2. 清理之前的方案
**移除文件/配置:**
- ❌ `package.json` 中的 `prebuild` 脚本
- ❌ `Dockerfile` 中的 `cp -r docs public/` 命令
- ❌ `.gitignore` 中的 `public/docs/` 规则
**保留文件:**
- ✅ `ecosystem.config.js` - PM2 配置
- ✅ `Dockerfile` - Docker 镜像配置(已简化)
- ✅ `docker-compose.yml` - 添加了卷挂载
- ✅ `.dockerignore` - 优化构建
- ✅ `scripts/clean.sh` - 清理脚本(已更新)
### 3. 文档更新
- ✅ `DEPLOYMENT.md` - 更新为卷挂载方案说明
- ✅ `docs/DOCKER_DOCS_SETUP.md` - 详细的卷挂载方案文档
- ✅ `QUICKSTART.md` - 更新快速参考
- ✅ `README.md` - 保持部署文档链接
## 🎯 使用方法
### 本地开发
```bash
yarn dev # 开发服务器
```
### 构建和预览
```bash
yarn build # 构建(不包含 docs
yarn preview # 预览
```
### Docker 部署
```bash
# 首次部署
docker-compose up -d --build
# 后续启动
docker-compose up -d
# 查看日志
docker-compose logs -f
```
### 更新文档
```bash
# 直接编辑即可,立即生效
vim docs/DESIGN_COOKBOOK.md
# 或使用编辑器
code docs/components/PageTitleBar.md
# 浏览器刷新即可看到更新
```
## 🔍 验证方法
### 1. 检查挂载
```bash
# 查看容器挂载情况
docker inspect nex-design-app | grep -A 10 Mounts
# 检查容器内文件
docker exec nex-design-app ls -la /app/dist/docs/
```
### 2. 验证实时同步
```bash
# 在宿主机添加测试文件
echo "# Test" > docs/test.md
# 立即在容器内查看
docker exec nex-design-app cat /app/dist/docs/test.md
# 清理测试文件
rm docs/test.md
```
### 3. 浏览器访问
```bash
# 启动服务
docker-compose up -d
# 访问文档
curl http://localhost:3000/docs/DESIGN_COOKBOOK.md
# 或在浏览器打开
open http://localhost:3000
```
## 📊 方案对比
| 特性 | 卷挂载方案 ✅ | 构建时复制方案 |
|------|--------------|----------------|
| 文档实时更新 | ✅ 立即生效 | ❌ 需要重新构建 |
| 镜像体积 | ✅ 更小 | ❌ 更大 |
| 构建速度 | ✅ 更快 | ❌ 更慢 |
| 维护便利性 | ✅ 直接编辑 | ❌ 需要构建 |
| 部署灵活性 | ✅ 高 | ⚠️ 中 |
| 镜像自包含 | ⚠️ 需要 docs 目录 | ✅ 完全自包含 |
## 🚨 注意事项
### 1. 部署要求
- 部署时需要确保 `docs/` 目录存在
- 使用 `git clone``scp -r` 时包含 docs 目录
### 2. 权限管理
```bash
# 确保 docs 目录权限正确
chmod -R 755 docs/
```
### 3. 生产环境
可以将 docs 部署到专门的目录:
```yaml
# docker-compose.prod.yml
volumes:
- /var/www/nex-design-docs:/app/dist/docs:ro
```
## 📚 相关文档
- **QUICKSTART.md** - 快速参考和常用命令
- **DEPLOYMENT.md** - 完整部署文档
- **docs/DOCKER_DOCS_SETUP.md** - 卷挂载方案详细说明
## 🎉 总结
**核心优势:**
- 修改文档 → 立即生效
- 简化构建流程
- 提升开发体验
**一行配置搞定:**
```yaml
volumes:
- ./docs:/app/dist/docs:ro
```
这就是你想要的方案!🎯

View File

@ -13,6 +13,7 @@ services:
- NODE_ENV=production
volumes:
- ./logs:/app/logs
- ./docs:/app/dist/docs
networks:
- nex-network
healthcheck:

View File

@ -0,0 +1,370 @@
# 文档目录 Docker 部署方案
## 问题描述
应用中 `/design` 路由通过 `fetch('/docs/...')` 加载项目根目录下 `docs/` 文件夹中的 Markdown 文档。在 Docker 容器中运行时,需要确保这些文档能够被访问。
## 解决方案Docker 卷挂载
采用 Docker 卷挂载方式,将宿主机的 `docs/` 目录直接映射到容器内,实现文档的实时更新和灵活管理。
### 配置方式
#### docker-compose.yml 配置
```yaml
version: '3.8'
services:
nex-design:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./logs:/app/logs # 日志目录挂载
- ./docs:/app/dist/docs:ro # 文档目录挂载(只读)
```
**说明:**
- `./docs:/app/dist/docs` - 将宿主机当前目录的 `docs` 映射到容器的 `/app/dist/docs`
- `:ro` - 只读挂载,提高安全性,防止容器内进程修改文档
### 方案优势
**实时更新**
- 修改 MD 文件后立即生效
- 无需重新构建 Docker 镜像
- 无需重启容器
**方便维护**
- 在宿主机直接编辑文档
- 使用熟悉的编辑器和工具
- 支持版本控制
**轻量镜像**
- Docker 镜像不包含文档内容
- 镜像体积更小
- 构建速度更快
**灵活部署**
- 可以独立管理文档版本
- 支持多环境部署(开发、测试、生产使用不同文档)
- 易于更新和回滚
### 目录结构
**宿主机:**
```
nex-design/
├── docs/ # 文档源文件Git 管理)
│ ├── DESIGN_COOKBOOK.md
│ ├── components/
│ │ ├── PageTitleBar.md
│ │ ├── ListTable.md
│ │ └── ...
│ └── pages/
├── dist/ # 构建产物(容器内)
│ ├── index.html
│ └── assets/
└── docker-compose.yml
```
**容器内:**
```
/app/
├── dist/ # 应用构建产物
│ ├── index.html
│ ├── assets/
│ └── docs/ # 挂载点 → 宿主机 docs/
├── logs/ # 日志目录(挂载)
└── ecosystem.config.js # PM2 配置
```
### 使用流程
#### 1. 启动服务
```bash
# 第一次启动(构建镜像)
docker-compose up -d --build
# 后续启动(使用已有镜像)
docker-compose up -d
```
#### 2. 修改文档
```bash
# 在宿主机直接编辑文档
vim docs/DESIGN_COOKBOOK.md
# 或使用 VS Code 等编辑器
code docs/components/PageTitleBar.md
```
#### 3. 验证更新
```bash
# 文档修改后立即生效,无需任何操作
# 浏览器刷新即可看到最新内容
# 或使用 curl 验证
curl http://localhost:3000/docs/DESIGN_COOKBOOK.md
```
### 验证挂载
```bash
# 检查容器内的挂载情况
docker exec nex-design-app ls -la /app/dist/docs/
# 查看某个文档内容
docker exec nex-design-app cat /app/dist/docs/DESIGN_COOKBOOK.md
# 验证文件同步
# 在宿主机修改文件
echo "# Test" >> docs/test.md
# 立即在容器内查看
docker exec nex-design-app cat /app/dist/docs/test.md
```
### 部署注意事项
#### 1. 生产环境部署
**方式一:携带 docs 目录**
```bash
# 使用 git clone 或 scp 上传整个项目
git clone <repo> /path/to/deploy
cd /path/to/deploy
docker-compose up -d
```
**方式二:单独管理文档**
```bash
# 文档单独部署在某个目录
mkdir -p /var/www/nex-design-docs
# 上传文档到该目录
# 修改 docker-compose.yml
volumes:
- /var/www/nex-design-docs:/app/dist/docs:ro
```
#### 2. 权限管理
```bash
# 确保 docs 目录有正确的权限
chmod -R 755 docs/
# 只读挂载可防止容器内修改,但宿主机权限仍需控制
```
#### 3. 多环境配置
可以为不同环境创建不同的 docker-compose 文件:
```bash
# docker-compose.dev.yml - 开发环境
volumes:
- ./docs:/app/dist/docs:ro
# docker-compose.prod.yml - 生产环境
volumes:
- /var/www/docs:/app/dist/docs:ro
```
使用时指定配置文件:
```bash
docker-compose -f docker-compose.prod.yml up -d
```
### 故障排查
#### 问题 1文档无法加载
```bash
# 检查挂载是否成功
docker inspect nex-design-app | grep -A 10 Mounts
# 检查容器内文件
docker exec nex-design-app ls -la /app/dist/docs/
# 检查文件权限
ls -la docs/
```
#### 问题 2修改后未生效
```bash
# 确认使用的是卷挂载而不是 COPY
docker exec nex-design-app cat /app/dist/docs/DESIGN_COOKBOOK.md
# 检查浏览器缓存
# 使用 Ctrl+Shift+R 强制刷新
# 检查 serve 是否缓存了静态文件
docker-compose restart
```
#### 问题 3Windows 路径问题
Windows 下需要注意路径格式:
```yaml
# 错误
volumes:
- .\docs:/app/dist/docs:ro
# 正确
volumes:
- ./docs:/app/dist/docs:ro
```
### 性能考虑
#### 1. 卷挂载性能
- **Linux/macOS**: 性能很好,几乎无损耗
- **Windows/macOS + Docker Desktop**: 可能有轻微性能损耗
- **生产环境**: 使用 Linux 主机,性能最佳
#### 2. 优化建议
如果文档很多且访问频繁,可考虑:
1. **使用命名卷**
```yaml
volumes:
docs-data:
driver: local
driver_opts:
type: none
o: bind
device: /path/to/docs
services:
nex-design:
volumes:
- docs-data:/app/dist/docs:ro
```
2. **缓存策略**
在 Nginx 反向代理中添加缓存:
```nginx
location /docs/ {
proxy_pass http://nex_design;
proxy_cache_valid 200 10m;
add_header X-Cache-Status $upstream_cache_status;
}
```
## 替代方案对比
### 方案 A: 构建时复制(未采用)
```dockerfile
# Dockerfile
COPY docs /app/dist/docs
```
❌ 缺点:
- 修改文档需要重新构建镜像
- 镜像体积更大
- 更新流程复杂
✅ 优点:
- 镜像自包含
- 适合不常修改的场景
### 方案 B: prebuild 脚本(未采用)
```json
{
"scripts": {
"prebuild": "cp -r docs public/"
}
}
```
❌ 缺点:
- 需要重新构建才能更新
- 增加构建时间
- 文档和代码耦合
### 方案 C: 卷挂载(✅ 当前采用)
```yaml
volumes:
- ./docs:/app/dist/docs:ro
```
✅ 优点:
- 实时更新
- 灵活管理
- 镜像轻量
⚠️ 注意:
- 需要保持 docs 目录结构
- 部署时需要文档文件
## 最佳实践
### 1. 文档版本管理
```bash
# 使用 Git 管理文档版本
cd docs
git log DESIGN_COOKBOOK.md
# 回滚到特定版本
git checkout <commit-hash> DESIGN_COOKBOOK.md
```
### 2. 文档自动化部署
```bash
#!/bin/bash
# scripts/update-docs.sh
echo "更新文档..."
cd /path/to/nex-design
# 拉取最新文档
git pull origin main -- docs/
# 无需重启容器,文档即时生效
echo "文档已更新!"
```
### 3. 监控文档访问
可以在 Nginx 中记录文档访问日志:
```nginx
location /docs/ {
access_log /var/log/nginx/docs-access.log;
proxy_pass http://nex_design;
}
```
## 总结
使用 Docker 卷挂载方案是最灵活、最适合本项目的解决方案:
**实时更新** - 修改即生效
**简单维护** - 直接编辑文件
**轻量镜像** - 更快的构建和部署
**灵活部署** - 支持多种场景
**核心配置只需一行:**
```yaml
volumes:
- ./docs:/app/dist/docs:ro
```
## 相关文件
- `docker-compose.yml:16` - 卷挂载配置
- `DEPLOYMENT.md:14-35` - 部署文档说明
- `QUICKSTART.md` - 快速参考

View File

@ -0,0 +1,593 @@
# ChartPanel 组件
## 组件说明
图表面板组件,基于 ECharts 封装,用于展示数据可视化图表。支持折线图、柱状图、饼图、环形图等多种图表类型,适合在仪表盘、监控面板、数据分析页面中使用。
## 组件位置
```
src/components/ChartPanel/ChartPanel.jsx
src/components/ChartPanel/ChartPanel.css
```
## 参数说明
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| type | string | 否 | 'line' | 图表类型line/bar/pie/ring |
| title | string | 否 | - | 图表标题 |
| data | ChartData | 是 | - | 图表数据 |
| height | number | 否 | 200 | 图表高度(像素) |
| option | object | 否 | {} | 自定义 ECharts 配置项 |
| className | string | 否 | '' | 自定义类名 |
### ChartData 数据格式
#### 折线图 / 柱状图数据格式
```typescript
{
xAxis: string[] // X 轴数据(时间、类别等)
series: Array<{
name: string // 系列名称
data: number[] // Y 轴数据
color?: string // 自定义颜色(可选)
}>
}
```
#### 饼图 / 环形图数据格式
```typescript
{
data: Array<{
name: string // 数据项名称
value: number // 数据值
color?: string // 自定义颜色(可选)
}>
}
```
### 图表类型说明
| 类型 | 适用场景 | 数据维度 |
|------|---------|---------|
| line | 趋势展示、时序数据、性能监控 | 多系列、时间轴 |
| bar | 对比分析、排名展示、统计数据 | 多系列、分类轴 |
| pie | 占比分析、分布展示 | 单维度、百分比 |
| ring | 占比分析(带中心文字) | 单维度、百分比 |
## 使用示例
### 折线图
```jsx
import ChartPanel from '../components/ChartPanel/ChartPanel'
const lineData = {
xAxis: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
series: [
{
name: 'CPU 使用率',
data: [30, 45, 65, 50, 70, 55],
color: '#1677ff',
},
{
name: '内存使用率',
data: [20, 35, 55, 45, 60, 50],
color: '#52c41a',
},
],
}
<ChartPanel
type="line"
title="性能监控"
data={lineData}
height={300}
/>
```
### 柱状图
```jsx
const barData = {
xAxis: ['周一', '周二', '周三', '周四', '周五'],
series: [
{
name: '新增用户',
data: [120, 200, 150, 180, 220],
color: '#1677ff',
},
{
name: '活跃用户',
data: [80, 150, 120, 140, 180],
color: '#52c41a',
},
],
}
<ChartPanel
type="bar"
title="用户统计"
data={barData}
height={250}
/>
```
### 饼图
```jsx
const pieData = {
data: [
{ name: '运行中', value: 45, color: '#52c41a' },
{ name: '已停止', value: 20, color: '#8c8c8c' },
{ name: '错误', value: 5, color: '#ff4d4f' },
{ name: '待部署', value: 30, color: '#faad14' },
],
}
<ChartPanel
type="pie"
title="状态分布"
data={pieData}
height={250}
/>
```
### 环形图
```jsx
const ringData = {
data: [
{ name: '在线', value: 85 },
{ name: '离线', value: 15 },
],
}
<ChartPanel
type="ring"
title="在线率"
data={ringData}
height={200}
/>
```
### 自定义 ECharts 配置
```jsx
<ChartPanel
type="line"
data={lineData}
option={{
grid: {
left: '5%',
right: '5%',
bottom: '10%',
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
},
xAxis: {
axisLabel: {
rotate: 45,
},
},
}}
/>
```
### 配合 SideInfoPanel 使用
```jsx
import SideInfoPanel from '../components/SideInfoPanel/SideInfoPanel'
import ChartPanel from '../components/ChartPanel/ChartPanel'
<SideInfoPanel
sections={[
{
key: 'performance',
title: '性能监控',
icon: <LineChartOutlined />,
content: (
<>
<ChartPanel
type="line"
title="CPU 使用率"
data={cpuData}
height={200}
/>
<ChartPanel
type="line"
title="内存使用率"
data={memoryData}
height={200}
/>
</>
),
},
{
key: 'distribution',
title: '状态分布',
icon: <PieChartOutlined />,
content: (
<>
<ChartPanel
type="ring"
title="在线状态"
data={statusData}
height={200}
/>
<ChartPanel
type="bar"
title="区域分布"
data={regionData}
height={200}
/>
</>
),
},
]}
/>
```
## DOM 结构层级
```html
<div class="chart-panel">
<!-- 图表标题(可选) -->
{title && (
<div class="chart-panel-title">
{title}
</div>
)}
<!-- ECharts 容器 -->
<div
ref={chartRef}
class="chart-panel-chart"
style="height: 200px"
>
<!-- ECharts 实例挂载点 -->
</div>
</div>
```
## ECharts 配置说明
### 折线图默认配置
```javascript
{
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
},
legend: {
bottom: 0,
left: 'center',
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true,
},
xAxis: {
type: 'category',
data: data.xAxis,
boundaryGap: false,
},
yAxis: {
type: 'value',
},
series: data.series.map(s => ({
name: s.name,
type: 'line',
data: s.data,
smooth: true,
itemStyle: { color: s.color },
})),
}
```
### 柱状图默认配置
```javascript
{
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
},
legend: {
bottom: 0,
left: 'center',
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true,
},
xAxis: {
type: 'category',
data: data.xAxis,
},
yAxis: {
type: 'value',
},
series: data.series.map(s => ({
name: s.name,
type: 'bar',
data: s.data,
itemStyle: { color: s.color },
})),
}
```
### 饼图默认配置
```javascript
{
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)',
},
legend: {
bottom: 0,
left: 'center',
},
series: [
{
type: 'pie',
radius: '70%',
center: ['50%', '45%'],
data: data.data.map(item => ({
name: item.name,
value: item.value,
itemStyle: { color: item.color },
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
```
### 环形图默认配置
```javascript
{
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)',
},
legend: {
bottom: 0,
left: 'center',
},
series: [
{
type: 'pie',
radius: ['50%', '70%'], // 环形图特征:内外半径
center: ['50%', '45%'],
data: data.data.map(item => ({
name: item.name,
value: item.value,
itemStyle: { color: item.color },
})),
label: {
show: false, // 环形图默认不显示标签
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
```
## 组件生命周期
### 初始化流程
1. 组件挂载时,通过 `useRef` 获取 DOM 容器
2. 使用 `echarts.init()` 初始化 ECharts 实例
3. 根据 `type``data` 生成配置项
4. 调用 `setOption()` 渲染图表
### 更新流程
1. 当 `type`、`data`、`option` 变化时触发 `useEffect`
2. 复用已有的 ECharts 实例
3. 重新生成配置项并调用 `setOption()` 更新图表
### 响应式处理
1. 监听 `window.resize` 事件
2. 调用 `chartInstance.resize()` 自动调整图表尺寸
3. 组件卸载时移除事件监听并销毁 ECharts 实例
```javascript
useEffect(() => {
const handleResize = () => {
if (chartInstance.current) {
chartInstance.current.resize()
}
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
if (chartInstance.current) {
chartInstance.current.dispose()
}
}
}, [])
```
## 样式定制
组件提供以下 CSS 类名供自定义样式:
- `.chart-panel` - 面板容器
- `.chart-panel-title` - 图表标题
- `.chart-panel-chart` - ECharts 容器
### 自定义样式示例
```css
/* 修改标题样式 */
.chart-panel-title {
font-size: 16px;
font-weight: 600;
color: #1677ff;
}
/* 修改面板背景 */
.chart-panel {
background: #f5f5f5;
border-radius: 8px;
padding: 16px;
}
/* 修改图表容器 */
.chart-panel-chart {
background: #ffffff;
}
```
## 使用场景
1. **性能监控面板** - 展示 CPU、内存、网络等实时性能数据
2. **数据分析仪表盘** - 展示业务指标、用户统计、销售数据
3. **状态分布展示** - 展示系统状态、设备状态、任务状态的占比
4. **趋势对比分析** - 展示多个指标的时序变化和对比
## 注意事项
1. **数据格式**
- 确保数据格式与图表类型匹配
- 折线图/柱状图使用 xAxis + series 格式
- 饼图/环形图使用 data 数组格式
- 数据更新时会自动重新渲染
2. **图表高度**
- 默认高度 200px建议根据内容调整
- 折线图/柱状图200-400px
- 饼图/环形图200-300px
- 避免高度过小导致显示不清
3. **颜色使用**
- 可在数据中指定 color 属性自定义颜色
- 不指定时使用 ECharts 默认配色
- 建议使用语义化颜色(绿色=正常,红色=错误)
- 同一页面保持配色风格一致
4. **性能优化**
- 大数据量时考虑数据采样
- 避免频繁更新图表(建议间隔 > 1s
- 组件卸载时会自动销毁 ECharts 实例
- 窗口 resize 时会自动调整图表尺寸
5. **响应式**
- 图表会自动适应容器宽度
- 窗口大小变化时自动 resize
- 小屏幕时建议减小图表高度
6. **自定义配置**
- 使用 option 参数传入自定义 ECharts 配置
- 自定义配置会与默认配置深度合并
- 可完全覆盖默认配置实现高度定制
7. **标题使用**
- title 为可选参数
- 在 SideInfoPanel 中使用时section 已有标题,可省略 ChartPanel 标题
- 独立使用时建议添加标题增强可读性
## 配合使用的组件
- **SideInfoPanel** - 侧边信息面板(推荐)
- **StatCard** - 统计卡片(配合使用)
- **SplitLayout** - 分栏布局
- **PageTitleBar** - 页面标题栏
## ECharts 依赖
本组件依赖 ECharts 库,确保已安装:
```bash
npm install echarts
# 或
yarn add echarts
```
导入方式:
```javascript
import * as echarts from 'echarts'
```
## 示例数据结构参考
### 性能监控数据
```javascript
const performanceData = {
xAxis: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
series: [
{
name: 'CPU',
data: [30, 45, 65, 50, 70, 55],
color: '#1677ff',
},
{
name: '内存',
data: [20, 35, 55, 45, 60, 50],
color: '#52c41a',
},
],
}
```
### 状态分布数据
```javascript
const statusData = {
data: [
{ name: '运行中', value: 45, color: '#52c41a' },
{ name: '已停止', value: 20, color: '#8c8c8c' },
{ name: '错误', value: 5, color: '#ff4d4f' },
{ name: '待部署', value: 30, color: '#faad14' },
],
}
```
### 区域分布数据
```javascript
const regionData = {
xAxis: ['华东', '华南', '华北', '西南', '西北'],
series: [
{
name: '服务器数量',
data: [120, 80, 95, 65, 45],
color: '#1677ff',
},
],
}
```

View File

@ -419,6 +419,8 @@ function UserListPage() {
## 布局结构
### 整体布局
抽屉采用固定头部、可滚动内容的布局:
```
@ -427,13 +429,81 @@ function UserListPage() {
│ [关闭] [标题] [徽标] [操作按钮] │
├─────────────────────────────────────┤
│ │
│ 内容区域(可滚动
│ 内容区域(可滚动padding: 24px
│ - children 主要内容 │
│ - tabs 标签页(可选) │
│ │
└─────────────────────────────────────┘
```
### DOM 结构层级
```html
<Drawer styles={{ body: { padding: 0 } }}>
<div class="detail-drawer-content">
<!-- 固定头部区域 -->
<div class="detail-drawer-header">
<div class="detail-drawer-header-left">
<Button class="detail-drawer-close-button" />
<div class="detail-drawer-header-info">
<span class="detail-drawer-title-icon">{icon}</span>
<h2 class="detail-drawer-title">{title}</h2>
<span class="detail-drawer-badge">{badge}</span>
</div>
</div>
<div class="detail-drawer-header-right">
{headerActions}
</div>
</div>
<!-- 可滚动内容区域 - 统一的 24px padding -->
<div class="detail-drawer-scrollable-content" style="padding: 24px">
<!-- 主要内容 (children) -->
{children}
<!-- 标签页区域(可选) -->
<div class="detail-drawer-tabs">
<Tabs>
<TabPane>
<div class="detail-drawer-tab-content">
{tab.content}
</div>
</TabPane>
</Tabs>
</div>
</div>
</div>
</Drawer>
```
### Padding 说明
**重要:为避免内容贴边,组件已经统一管理 padding**
- ✅ **Drawer body**: `padding: 0`(由组件设置)
- ✅ **detail-drawer-scrollable-content**: `padding: 24px`(统一的外边距)
- ❌ **children 内容**: 不需要额外添加 padding
- ❌ **tab content**: 不需要额外添加 padding
**正确用法:**
```jsx
<DetailDrawer visible={true} onClose={onClose}>
<InfoPanel data={data} fields={fields} /> {/* ✅ 不需要额外 padding */}
</DetailDrawer>
```
**错误用法:**
```jsx
<DetailDrawer visible={true} onClose={onClose}>
<div style={{ padding: 24 }}> {/* ❌ 不要添加额外的 padding */}
<InfoPanel data={data} fields={fields} />
</div>
</DetailDrawer>
```
## 样式定制
组件提供以下 CSS 类名供自定义样式:
@ -460,10 +530,10 @@ function UserListPage() {
## 注意事项
1. 抽屉宽度默认 1080px可根据内容调整建议取值范围720-1200px
2. 标题栏固定在顶部,不随内容滚动,确保操作按钮始终可见
3. `children` 内容区域会自动应用内边距,`tabs` 内容需要自行控制样式
4. 操作按钮数量不宜过多,建议不超过 3 个
5. 使用 `tabs` 时,第一个标签页默认激活
6. 关闭抽屉时建议清空选中状态,避免下次打开时显示旧数据
7. 配合 InfoPanel 使用时InfoPanel 会自动处理内边距
1. **宽度选择**抽屉宽度默认 1080px可根据内容调整建议取值范围720-1200px
2. **固定头部**标题栏固定在顶部,不随内容滚动,确保操作按钮始终可见
3. **内容 padding**`detail-drawer-scrollable-content` 已经统一设置了 24px paddingchildren 内容不需要额外添加 padding
4. **操作按钮**操作按钮数量不宜过多,建议不超过 3 个
5. **标签页**使用 `tabs` 时,第一个标签页默认激活
6. **状态清理**关闭抽屉时建议清空选中状态,避免下次打开时显示旧数据
7. **InfoPanel 集成**配合 InfoPanel 使用时InfoPanel 会自动处理内部样式,不需要额外的容器包裹

View File

@ -0,0 +1,499 @@
# ExtendInfoPanel 组件
## 组件说明
扩展信息面板组件,用于展示多个可折叠的信息区块。支持横向和纵向两种布局模式,每个区块独立管理展开/收起状态,支持自定义图标和内容。适合在页面的扩展信息区(右侧或顶部)使用。
> **注意**:该组件由 SideInfoPanel 重命名而来,功能完全兼容。
## 组件位置
```
src/components/ExtendInfoPanel/ExtendInfoPanel.jsx
src/components/ExtendInfoPanel/ExtendInfoPanel.css
```
## 参数说明
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| sections | Array<SectionConfig> | 否 | [] | 信息区块配置数组 |
| layout | string | 否 | 'vertical' | 布局方式:'vertical'(垂直堆叠)\| 'horizontal'(水平排列) |
| className | string | 否 | '' | 自定义类名 |
### SectionConfig 配置项
| 属性名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| key | string | 是 | 区块唯一标识 |
| title | string | 是 | 区块标题 |
| icon | ReactNode | 否 | 标题图标 |
| content | ReactNode | 是 | 区块内容 |
| defaultCollapsed | boolean | 否 | 是否默认折叠,默认 false |
| hideTitleBar | boolean | 否 | 是否隐藏标题栏,默认 false隐藏后区块内容始终显示 |
## 布局模式
### 垂直布局Vertical
区块垂直堆叠排列,适合在右侧信息面板中使用。
```
┌────────────────┐
│ Section 1 │
├────────────────┤
│ Section 2 │
├────────────────┤
│ Section 3 │
└────────────────┘
```
### 水平布局Horizontal
区块水平排列,适合在顶部扩展面板中使用。
```
┌──────┬──────┬──────┐
│ Sec1 │ Sec2 │ Sec3 │
└──────┴──────┴──────┘
```
## 使用示例
### 基础用法 - 垂直布局
```jsx
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
import { DashboardOutlined } from '@ant-design/icons'
function MyPage() {
return (
<ExtendInfoPanel
layout="vertical"
sections={[
{
key: 'overview',
title: '概览',
icon: <DashboardOutlined />,
content: <div>概览内容</div>,
},
]}
/>
)
}
```
### 水平布局 - 用于顶部扩展区(隐藏标题栏)
```jsx
<ExtendInfoPanel
layout="horizontal"
sections={[
{
key: 'stats',
title: '数据统计',
hideTitleBar: true, // 隐藏标题栏,内容始终显示
content: (
<div className="stat-cards-grid stat-cards-grid-4">
<StatCard title="总数" value={100} />
<StatCard title="在线" value={85} />
<StatCard title="离线" value={10} />
<StatCard title="错误" value={5} />
</div>
),
},
]}
/>
```
### 水平布局 - 多个区块
```jsx
<ExtendInfoPanel
layout="horizontal"
sections={[
{
key: 'stats',
title: '数据统计',
content: (
<div style={{ display: 'flex', gap: '16px' }}>
<StatCard title="总数" value={100} />
<StatCard title="在线" value={85} />
</div>
),
},
]}
/>
```
### 多个区块
```jsx
<ExtendInfoPanel
sections={[
{
key: 'info',
title: '基本信息',
icon: <InfoCircleOutlined />,
content: <div>基本信息内容</div>,
},
{
key: 'stats',
title: '统计数据',
icon: <BarChartOutlined />,
content: <div>统计数据内容</div>,
},
{
key: 'logs',
title: '操作日志',
icon: <FileTextOutlined />,
defaultCollapsed: true, // 默认折叠
content: <div>日志内容</div>,
},
]}
/>
```
### 配合 StatCard 使用
```jsx
import StatCard from '../components/StatCard/StatCard'
<ExtendInfoPanel
sections={[
{
key: 'overview',
title: '概览',
icon: <DashboardOutlined />,
content: (
<div style={{ display: 'grid', gap: '12px' }}>
<StatCard
key="total"
title="镜像总数"
value={32}
icon={<DatabaseOutlined />}
color="blue"
gridColumn="1 / -1"
/>
<StatCard
key="running"
title="运行中"
value={28}
color="green"
/>
<StatCard
key="stopped"
title="已停止"
value={4}
color="gray"
/>
</div>
),
},
]}
/>
```
### 配合 ChartPanel 使用
```jsx
import ChartPanel from '../components/ChartPanel/ChartPanel'
<ExtendInfoPanel
sections={[
{
key: 'charts',
title: '数据可视化',
icon: <LineChartOutlined />,
content: (
<>
<ChartPanel
key="cpu"
type="line"
title="CPU使用率趋势"
data={cpuData}
height={180}
/>
<ChartPanel
key="memory"
type="line"
title="内存使用率趋势"
data={memoryData}
height={180}
/>
</>
),
},
]}
/>
```
### 完整示例 - 配合 SplitLayout
#### 横向布局(右侧扩展区)
```jsx
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
<SplitLayout
direction="horizontal"
mainContent={
<>
<ListActionBar ... />
<ListTable ... />
</>
}
extendContent={
<ExtendInfoPanel
layout="vertical" // 区块垂直堆叠
sections={[
{
key: 'overview',
title: '概览',
icon: <DashboardOutlined />,
content: <StatCards />
},
{
key: 'monitor',
title: '性能监控',
icon: <LineChartOutlined />,
content: <Charts />
}
]}
/>
}
/>
```
#### 纵向布局(顶部扩展区)
```jsx
import { useState } from 'react'
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
function UserListPage() {
const [showStats, setShowStats] = useState(false)
return (
<>
<PageTitleBar
title="用户列表"
showToggle={true}
onToggle={setShowStats}
/>
<SplitLayout
direction="vertical"
mainContent={
<>
<ListActionBar ... />
<ListTable ... />
</>
}
extendContent={
<ExtendInfoPanel
layout="horizontal" // 区块水平排列
sections={[
{
key: 'stats',
title: '数据统计',
content: (
<div style={{ display: 'flex', gap: '16px' }}>
<StatCard title="总用户数" value={100} />
<StatCard title="启用" value={85} color="green" />
<StatCard title="停用" value={15} color="gray" />
<StatCard title="筛选结果" value={85} color="orange" />
</div>
)
}
]}
/>
}
showExtend={showStats}
/>
</>
)
}
```
## 布局结构
### DOM 结构层级
#### 垂直布局
```html
<div class="extend-info-panel extend-info-panel-vertical">
<!-- 信息区块 1 -->
<div class="extend-info-section">
<!-- 区块头部(可点击) -->
<div class="extend-info-section-header" onClick={toggleSection}>
<div class="extend-info-section-title">
<span class="extend-info-section-icon">{icon}</span>
<span>{title}</span>
</div>
<button class="extend-info-section-toggle">
{isCollapsed ? <DownOutlined /> : <UpOutlined />}
</button>
</div>
<!-- 区块内容(折叠时隐藏) -->
{!isCollapsed && (
<div class="extend-info-section-content">
{content}
</div>
)}
</div>
<!-- 更多区块... -->
</div>
```
#### 水平布局
```html
<div class="extend-info-panel extend-info-panel-horizontal">
<div class="extend-info-section">...</div>
<div class="extend-info-section">...</div>
<div class="extend-info-section">...</div>
</div>
```
## 样式定制
组件提供以下 CSS 类名供自定义样式:
- `.extend-info-panel` - 面板容器
- `.extend-info-panel-vertical` - 垂直布局模式
- `.extend-info-panel-horizontal` - 水平布局模式
- `.extend-info-section` - 单个信息区块
- `.extend-info-section-header` - 区块头部
- `.extend-info-section-title` - 区块标题
- `.extend-info-section-icon` - 标题图标
- `.extend-info-section-toggle` - 折叠按钮
- `.extend-info-section-content` - 区块内容
### 自定义样式示例
```css
/* 修改区块间距 */
.extend-info-panel-vertical {
gap: 20px;
}
/* 自定义区块头部背景 */
.extend-info-section-header {
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
}
/* 修改水平布局的区块宽度 */
.extend-info-panel-horizontal .extend-info-section {
flex: 1;
min-width: 300px;
}
```
## 使用场景
### 1. 右侧信息面板(垂直布局)
- **系统监控面板**:展示系统状态、性能指标、告警信息
- **数据分析侧边栏**:展示统计数据、图表、筛选器
- **详情页辅助信息**:展示相关数据、操作历史、关联信息
### 2. 顶部扩展面板(水平布局)
- **统计数据面板**:展示多个统计卡片
- **快捷操作区**:展示常用操作和快捷入口
- **筛选条件区**:展示可展开的筛选条件
## 注意事项
### 1. 布局选择
```jsx
// ✅ 右侧扩展区使用垂直布局
<SplitLayout direction="horizontal">
<ExtendInfoPanel layout="vertical" />
</SplitLayout>
// ✅ 顶部扩展区使用水平布局
<SplitLayout direction="vertical">
<ExtendInfoPanel layout="horizontal" />
</SplitLayout>
// ❌ 避免:右侧扩展区使用水平布局(宽度不够)
<SplitLayout direction="horizontal">
<ExtendInfoPanel layout="horizontal" />
</SplitLayout>
```
### 2. 区块数量
- **垂直布局**:建议 2-4 个区块,过多影响用户体验
- **水平布局**:建议 1-4 个区块,根据容器宽度调整
### 3. 折叠状态
- 区块的折叠状态由组件内部管理,外部无法直接控制
- 可通过 `defaultCollapsed` 设置初始状态
- 建议将不常用的区块设为默认折叠
### 4. 内容高度
- **垂直布局**:区块内容高度不限,但建议单个区块不要过长(建议 < 500px
- **水平布局**:建议控制区块内容高度一致,保持视觉整齐
### 5. 图标使用
- 建议为每个区块添加图标,提升视觉识别度
- 图标应与区块内容相关
- 使用 Ant Design 图标库保持风格统一
### 6. 宽度适配
- **垂直布局**:组件自适应父容器宽度,建议在 320-400px 容器中使用
- **水平布局**:组件占满父容器宽度,区块平均分配或根据内容自适应
## 迁移指南
### 从 SideInfoPanel 迁移
组件功能完全兼容,只需更改导入路径和组件名:
**旧代码**
```jsx
import SideInfoPanel from '../components/SideInfoPanel/SideInfoPanel'
<SideInfoPanel sections={[...]} />
```
**新代码**
```jsx
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
<ExtendInfoPanel sections={[...]} />
```
**新增参数**
- `layout` - 布局模式(新增),默认 'vertical'
## 配合使用的组件
- **SplitLayout** - 布局容器(必需)
- **StatCard** - 统计卡片(推荐)
- **ChartPanel** - 图表面板(推荐)
- **InfoPanel** - 信息展示面板
- **PageTitleBar** - 页面标题栏(配合纵向布局)
## 相关文档
- [主内容区布局](../layouts/content-area-layout.md) - 详细的布局使用指南
- [SplitLayout](./SplitLayout.md) - 布局容器组件
- [StatCard](./StatCard.md) - 统计卡片组件
- [ChartPanel](./ChartPanel.md) - 图表面板组件

View File

@ -256,6 +256,8 @@ const fields = [
## 布局说明
### 栅格布局
组件使用 Ant Design 的 24 栅格系统:
```
@ -276,6 +278,51 @@ const fields = [
- `span=12` - 一行 2 列
- `span=24` - 占满整行(适合描述、备注等长文本字段)
### DOM 结构层级
```html
<div class="info-panel" style="padding: 0">
<!-- 信息展示区域 -->
<Row gutter={[24, 16]} style="padding: 24px">
<Col span={6}>
<div class="info-panel-item">
<div class="info-panel-label">用户名</div>
<div class="info-panel-value">admin</div>
</div>
</Col>
<Col span={6}>
<div class="info-panel-item">
<div class="info-panel-label">姓名</div>
<div class="info-panel-value">系统管理员</div>
</div>
</Col>
<!-- 更多字段... -->
</Row>
<!-- 操作按钮区域(可选) -->
<div class="info-panel-actions" style="padding: 24px 32px">
<Space>
<Button>转移分组</Button>
<Button>重置密码</Button>
</Space>
</div>
</div>
```
### Padding 说明
**当在 DetailDrawer 中使用时:**
- ✅ **DetailDrawer scrollable-content**: `padding: 24px`(外层统一边距)
- ✅ **info-panel**: `padding: 0`(无额外 padding
- ✅ **info-panel Row**: `padding: 24px`(内容区内边距)
- ✅ **info-panel-actions**: `padding: 24px 32px`(操作区内边距)
**独立使用时:**
组件会自动处理内部 padding无需额外设置。
## 样式定制
组件提供以下 CSS 类名供自定义样式:

View File

@ -367,6 +367,24 @@ function UserListPage() {
- `.list-table-container` - 表格容器
- `.row-selected` - 选中行的类名
### 固定高度设计
ListTable 组件采用固定表格体高度设计,确保页面布局的稳定性:
- **尺寸**:使用 Ant Design `size="middle"` 属性
- **行高**47pxAnt Design middle 尺寸默认值)
- **表格体高度**470px固定高度内部滚动
- **显示行数**10 行470px ÷ 47px = 10
- **分页器高度**56pxAnt Design 默认)
- **容器内边距**16px × 2
**设计说明**
- 组件使用 Ant Design 的标准 `size="middle"` 属性,不自定义行高
- 表格体固定 470px 高度,恰好显示 10 行数据
- 超过 10 行的内容通过表格体内部滚动查看
- 当数据不足 10 行时,表格体仍保持 470px 高度,确保布局稳定
- 分页器上边距 16px与表格体保持适当间距
## 使用场景
1. **用户列表** - 显示和管理用户数据

View File

@ -20,8 +20,8 @@ src/components/PageTitleBar/PageTitleBar.css
| description | string | 否 | - | 页面描述文本,显示在标题下方 |
| actions | ReactNode | 否 | - | 右侧操作按钮区域内容 |
| showToggle | boolean | 否 | false | 是否显示展开/收起按钮 |
| onToggle | function(expanded: boolean) | 否 | - | 展开/收起状态变化时的回调函数 |
| defaultExpanded | boolean | 否 | true | 默认展开状态 |
| onToggle | function(expanded: boolean) | 否 | - | 展开/收起状态变化时的回调函数,接收当前展开状态 |
| defaultExpanded | boolean | 否 | false | 默认展开状态 |
## 使用示例
@ -59,10 +59,12 @@ import { Tag } from 'antd'
```jsx
import { useState } from 'react'
import { Card, Row, Col, Statistic } from 'antd'
import { DesktopOutlined, CheckCircleOutlined, CloseCircleOutlined, SearchOutlined } from '@ant-design/icons'
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
function MyPage() {
const [showStatsPanel, setShowStatsPanel] = useState(true)
const [showStatsPanel, setShowStatsPanel] = useState(false)
return (
<div>
@ -70,13 +72,55 @@ function MyPage() {
title="主机列表"
description="查看和管理所有接入的主机终端"
showToggle={true}
defaultExpanded={false}
onToggle={(expanded) => setShowStatsPanel(expanded)}
/>
{/* 可展开/收起的内容区域 */}
{/* 可展开/收起的统计面板 */}
{showStatsPanel && (
<div className="stats-panel">
{/* 统计面板内容 */}
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card hoverable>
<Statistic
title="总主机数"
value={50}
prefix={<DesktopOutlined />}
valueStyle={{ color: '#1677ff' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card hoverable>
<Statistic
title="在线主机"
value={35}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card hoverable>
<Statistic
title="离线主机"
value={15}
prefix={<CloseCircleOutlined />}
valueStyle={{ color: '#8c8c8c' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="筛选结果"
value={50}
prefix={<SearchOutlined />}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
</Row>
</div>
)}
</div>
@ -106,7 +150,7 @@ import { PlusOutlined } from '@ant-design/icons'
组件提供以下 CSS 类名供自定义样式:
- `.page-title-bar` - 组件根容器
- `.title-bar-content` - 内容容器
- `.title-bar-content` - 标题栏内容容器
- `.title-bar-left` - 左侧内容区域
- `.title-bar-right` - 右侧内容区域
- `.title-group` - 标题和徽章组合
@ -120,12 +164,26 @@ import { PlusOutlined } from '@ant-design/icons'
1. **列表页面** - 显示列表页面的标题和描述
2. **详情页面** - 显示详情页的标题和状态标签
3. **带统计面板的页面** - 配合展开/收起功能控制统计信息的显示
3. **带统计面板的页面** - 通过展开/收起按钮控制外部统计面板的显示,保持页面简洁
4. **需要快捷操作的页面** - 通过 actions 参数在标题栏添加常用操作按钮
## 设计理念
PageTitleBar 采用**分体式设计**,组件本身只负责标题栏的展示和展开/收起状态管理,不包含扩展内容。这种设计具有以下优势:
1. **职责单一** - 组件专注于标题栏功能,代码更清晰
2. **灵活性高** - 扩展内容由父组件管理,可以自由定制样式和布局
3. **易于维护** - 标题栏和扩展内容相互独立,修改互不影响
4. **复用性强** - 同一个 PageTitleBar 可以控制不同类型的扩展内容
## 注意事项
1. `title` 参数为必填项,建议简洁明了
2. 当使用 `showToggle` 时,建议同时提供 `onToggle` 回调以响应状态变化
3. `badge` 参数支持任何 React 节点,常用的如 Ant Design 的 Tag、Badge 组件
4. `actions` 区域不建议放置过多按钮,以保持界面简洁
2. 当使用 `showToggle` 时:
- 必须提供 `onToggle` 回调函数来响应状态变化
- 在父组件中管理扩展内容的显示/隐藏状态
- 扩展内容应该独立于 PageTitleBar 组件之外渲染
3. `defaultExpanded` 默认值为 `false`,扩展面板默认收起,推荐保持此默认值以保持页面简洁
4. `badge` 参数支持任何 React 节点,常用的如 Ant Design 的 Tag、Badge 组件
5. `actions` 区域不建议放置过多按钮,以保持界面简洁
6. 扩展内容(如统计面板)应该在父组件中独立管理,不要嵌入到 PageTitleBar 内部

View File

@ -8,95 +8,214 @@
### 页面布局组件
1. **[PageTitleBar](./PageTitleBar.md)** - 页面标题栏组件
1. **PageTitleBar** - 页面标题栏组件
- 显示页面标题、描述和操作按钮
- 支持展开/收起功能
- 适用于所有页面的顶部区域
### 布局容器组件
2. **SplitLayout** - 主内容区布局容器
- 支持横向(左右)和纵向(上下)分栏
- 主内容区 + 扩展信息区
- 响应式设计
3. **ExtendInfoPanel** - 扩展信息面板
- 多个可折叠的信息区块
- 支持垂直堆叠和水平排列
- 配合 SplitLayout 使用
### 列表相关组件
2. **[ListActionBar](./ListActionBar.md)** - 列表操作栏组件
4. **ListActionBar** - 列表操作栏组件
- 提供操作按钮、搜索、筛选功能
- 适用于列表页面的顶部操作区
3. **[TreeFilterPanel](./TreeFilterPanel.md)** - 树形筛选面板组件
5. **TreeFilterPanel** - 树形筛选面板组件
- 树形结构的数据筛选
- 支持搜索和多级展开
- 配合 ListActionBar 使用
4. **[ListTable](./ListTable.md)** - 列表表格组件
6. **ListTable** - 列表表格组件
- 统一的表格样式和交互
- 支持行选择、分页、排序
- 适用于所有列表页面
### 详情展示组件
5. **[DetailDrawer](./DetailDrawer.md)** - 详情抽屉组件
7. **DetailDrawer** - 详情抽屉组件
- 从右侧滑出的详情面板
- 支持标签页和操作按钮
- 固定头部,内容可滚动
6. **[InfoPanel](./InfoPanel.md)** - 信息展示面板组件
8. **InfoPanel** - 信息展示面板组件
- 网格布局展示结构化数据
- 支持自定义字段渲染
- 配合 DetailDrawer 使用
### 数据展示组件
9. **StatCard** - 统计卡片组件
- 展示数值型统计数据
- 支持图标、颜色主题、趋势指示器
- 支持网格布局(单列/双列)
- 配合 ExtendInfoPanel 使用
10. **ChartPanel** - 图表面板组件
- 基于 ECharts 的图表展示
- 支持折线图、柱状图、饼图、环形图
- 自适应容器尺寸
- 配合 ExtendInfoPanel 使用
### 交互反馈组件
7. **[ConfirmDialog](./ConfirmDialog.md)** - 确认对话框组件
- 提供统一的确认对话框样式
- 支持删除、警告、通用确认等场景
- 支持异步操作
11. **ConfirmDialog** - 确认对话框组件
- 提供统一的确认对话框样式
- 支持删除、警告、通用确认等场景
- 支持异步操作
8. **[Toast](./Toast.md)** - 通知反馈组件
- 操作完成后的提示信息
- 支持成功、错误、警告、信息四种类型
- 从右上角滑出,自动消失
12. **Toast** - 通知反馈组件
- 操作完成后的提示信息
- 支持成功、错误、警告、信息四种类型
- 从右上角滑出,自动消失
## 组件关系图
### 页面布局结构
#### 横向布局(左右分栏)
适用场景:需要持续展示扩展信息的页面(如监控页面、数据分析页面)
```
┌─────────────────────────────────────────────────────────┐
│ PageTitleBar (页面标题栏) │
├─────────────────────────────────┬───────────────────────┤
│ │ │
│ 主内容区 (Main Content) │ 扩展信息区 │
│ │ (Extend Info) │
│ ┌───────────────────────────┐ │ ┌─────────────────┐ │
│ │ ListActionBar (操作栏) │ │ │ ExtendInfoPanel │ │
│ │ ├─ 操作按钮 │ │ │ │ │
│ │ ├─ 搜索框 │ │ │ - 概览区块 │ │
│ │ └─ TreeFilterPanel │ │ │ - 图表区块 │ │
│ ├───────────────────────────┤ │ │ - 监控区块 │ │
│ │ ListTable (数据表格) │ │ │ │ │
│ │ └─ 点击行 → DetailDrawer │ │ │ (StatCard + │ │
│ └───────────────────────────┘ │ │ ChartPanel) │ │
│ │ └─────────────────┘ │
│ SplitLayout (direction="horizontal") │
└─────────────────────────────────┴───────────────────────┘
```
页面结构层次:
┌─────────────────────────────────────────┐
│ PageTitleBar (页面标题栏) │
├─────────────────────────────────────────┤
│ ListActionBar (操作栏) │
│ ├─ 操作按钮 │
│ ├─ 搜索框 │
│ └─ TreeFilterPanel (筛选面板) │
├─────────────────────────────────────────┤
│ ListTable (数据表格) │
│ └─ 点击行 → DetailDrawer │
├─────────────────────────────────────────┤
│ DetailDrawer (详情抽屉) │
│ ├─ InfoPanel (基本信息) │
│ └─ Tabs (关联数据标签页) │
└─────────────────────────────────────────┘
#### 纵向布局(上下分栏)
交互反馈:
适用场景:需要可展开/收起的统计面板(如用户列表、主机列表)
```
┌─────────────────────────────────────────────────────────┐
│ PageTitleBar (页面标题栏 + Toggle 控制) │
├─────────────────────────────────────────────────────────┤
│ │
│ 扩展信息区 (Extend Info - 可展开/收起) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ExtendInfoPanel (layout="horizontal") │ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ 总数 │ │ 在线 │ │ 离线 │ │ 筛选 │ │ │
│ │ │ Card │ │ Card │ │ Card │ │ Card │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ │ (StatCard 组件,水平排列) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────┤
│ │
│ 主内容区 (Main Content) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ListActionBar (操作栏) │ │
│ │ ├─ 操作按钮 │ │
│ │ ├─ 搜索框 │ │
│ │ └─ TreeFilterPanel (高级筛选) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ ListTable (数据表格) │ │
│ │ └─ 点击行 → DetailDrawer │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ SplitLayout (direction="vertical", extendPosition="top") │
└─────────────────────────────────────────────────────────┘
```
详情抽屉 (从右侧滑出):
```
┌─────────────────────────────────┐
│ DetailDrawer │
│ ├─ InfoPanel (基本信息) │
│ └─ Tabs (关联数据标签页) │
└─────────────────────────────────┘
交互反馈流程:
操作 → ConfirmDialog (确认) → Toast (结果反馈)
```
## 组件组合示例
### 组件依赖关系
### 标准列表页面
```
PageTitleBar (独立使用)
SplitLayout (布局容器)
├─ mainContent
│ ├─ ListActionBar
│ │ └─ TreeFilterPanel
│ └─ ListTable
│ └─ DetailDrawer
│ └─ InfoPanel
└─ extendContent
└─ ExtendInfoPanel
├─ StatCard
└─ ChartPanel
ConfirmDialog (全局调用)
Toast (全局调用)
```
## 典型页面组合
### 1. 标准列表页面(横向布局)
```jsx
<PageTitleBar title="用户列表" description="..." />
<PageTitleBar title="虚拟机镜像" description="..." />
<ListActionBar
actions={[...]}
search={{...}}
filter={{
content: <TreeFilterPanel {...} />
}}
/>
<ListTable
columns={columns}
dataSource={data}
onRowClick={showDetail}
<SplitLayout
direction="horizontal"
mainContent={
<>
<ListActionBar
actions={[...]}
search={{...}}
/>
<ListTable
columns={columns}
dataSource={data}
onRowClick={showDetail}
/>
</>
}
extendContent={
<ExtendInfoPanel
sections={[
{
key: 'overview',
title: '概览',
content: <StatCards />
},
{
key: 'monitor',
title: '性能监控',
content: <Charts />
}
]}
/>
}
/>
<DetailDrawer visible={showDrawer}>
@ -104,7 +223,49 @@
</DetailDrawer>
```
### 删除操作流程
### 2. 带统计面板的列表页面(纵向布局)
```jsx
<PageTitleBar
title="用户列表"
showToggle={true}
onToggle={setShowStats}
/>
<SplitLayout
direction="vertical"
mainContent={
<>
<ListActionBar
actions={[...]}
search={{...}}
filter={{
content: <TreeFilterPanel {...} />
}}
/>
<ListTable
columns={columns}
dataSource={data}
/>
</>
}
extendContent={
<ExtendInfoPanel
layout="horizontal"
sections={[
{
key: 'stats',
title: '数据统计',
content: <StatCards />
}
]}
/>
}
showExtend={showStats}
/>
```
### 3. 删除操作流程
```jsx
// 1. 点击删除按钮
@ -122,13 +283,42 @@ ConfirmDialog.delete({
})
```
## 组件选择指南
### 布局组件
| 场景 | 推荐组件 | 说明 |
|------|---------|------|
| 需要右侧信息面板 | SplitLayout (horizontal) + ExtendInfoPanel | 监控页面、数据分析页面 |
| 需要顶部统计面板 | SplitLayout (vertical) + ExtendInfoPanel | 可展开的统计信息 |
| 简单列表页 | ListActionBar + ListTable | 无扩展信息需求 |
### 数据展示组件
| 数据类型 | 推荐组件 | 说明 |
|---------|---------|------|
| 统计数值 | StatCard | 简洁的数值展示 |
| 趋势图表 | ChartPanel (line) | 时间序列数据 |
| 分布数据 | ChartPanel (pie/ring) | 占比分析 |
| 对比数据 | ChartPanel (bar) | 类别对比 |
| 结构化信息 | InfoPanel | 对象详细属性 |
### 交互组件
| 场景 | 推荐组件 | 说明 |
|------|---------|------|
| 危险操作确认 | ConfirmDialog.delete | 删除确认 |
| 一般操作确认 | ConfirmDialog.confirm | 普通确认 |
| 操作结果反馈 | Toast | 成功/失败提示 |
| 筛选数据 | TreeFilterPanel | 树形结构筛选 |
## 使用指南
### 开始使用
1. 查看对应组件的详细文档
2. 了解组件的参数配置
3. 参考示例代码
1. 从左侧菜单选择组件查看详细文档
2. 了解组件的参数配置和使用场景
3. 参考示例代码进行开发
4. 根据实际需求调整参数
### 设计原则
@ -137,13 +327,29 @@ ConfirmDialog.delete({
- **可复用** - 组件高度封装,易于复用
- **可配置** - 提供丰富的配置选项
- **易用性** - API 设计简洁直观
- **响应式** - 自适应不同屏幕尺寸
### 技术栈
- React 18
- Ant Design 5.x
- ECharts 5.x
- CSS Modules
### 命名规范
- **组件名**PascalCase如 PageTitleBar
- **参数名**camelCase如 showToggle
- **CSS 类名**kebab-case如 page-title-bar
- **文件名**:与组件名一致(如 PageTitleBar.jsx
## 更新记录
- 2025-11-13: 新增布局系统SplitLayout + ExtendInfoPanel
- 2025-11-13: 新增数据展示组件StatCard + ChartPanel
- 2025-11-04: 初始版本,包含 8 个核心组件文档
## 相关文档
- **主内容区布局** - 详细的布局使用指南
- **设计手册** - 设计规范和最佳实践

View File

@ -0,0 +1,447 @@
# SplitLayout 组件
## 组件说明
主内容区布局组件,支持横向(左右)和纵向(上下)两种分栏模式。用于将页面划分为主内容区和扩展信息区,支持响应式设计和灵活的布局配置。
## 组件位置
```
src/components/SplitLayout/SplitLayout.jsx
src/components/SplitLayout/SplitLayout.css
```
## 参数说明
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| direction | string | 否 | 'horizontal' | 布局方向:'horizontal'(左右分栏)\| 'vertical'(上下分栏) |
| mainContent | ReactNode | 是 | - | 主内容区 |
| extendContent | ReactNode | 否 | - | 扩展内容区 |
| extendSize | number | 否 | 360 | 扩展区尺寸horizontal 模式下为宽度px |
| gap | number | 否 | 16 | 主内容与扩展区间距px |
| showExtend | boolean | 否 | true | 是否显示扩展区 |
| extendPosition | string | 否 | 根据 direction 自动设置 | 扩展区位置horizontal 模式默认 'right'vertical 模式默认 'top' |
| className | string | 否 | '' | 自定义类名 |
## 布局模式
### 横向布局Horizontal
左右分栏,主内容在左,扩展信息在右。
**布局结构**
```
┌──────────────────────────┬──────────────────┐
│ │ │
│ Main Content │ Extend Info │
│ (flex: 1) │ (固定宽度) │
│ │ (可独立滚动) │
│ │ │
└──────────────────────────┴──────────────────┘
```
**适用场景**
- 数据列表 + 统计信息面板
- 监控页面 + 实时数据面板
- 表单编辑 + 帮助文档
### 纵向布局Vertical
上下分栏,扩展信息在上,主内容在下。
**布局结构**
```
┌─────────────────────────────────────────┐
│ Extend Info (高度自适应, 可收起) │
├─────────────────────────────────────────┤
│ │
│ Main Content │
│ │
│ │
└─────────────────────────────────────────┘
```
**适用场景**
- 带统计面板的列表页
- 可展开/收起的筛选条件区
- 概览信息 + 详细列表
## 使用示例
### 基础用法 - 横向布局
```jsx
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ListTable from '../components/ListTable/ListTable'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
function MyPage() {
return (
<SplitLayout
direction="horizontal"
mainContent={
<ListTable columns={columns} dataSource={data} />
}
extendContent={
<ExtendInfoPanel sections={sections} />
}
/>
)
}
```
### 纵向布局 + 可折叠
```jsx
import { useState } from 'react'
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
function MyPage() {
const [showStats, setShowStats] = useState(false)
return (
<>
<PageTitleBar
title="用户列表"
showToggle={true}
defaultExpanded={false}
onToggle={setShowStats}
/>
<SplitLayout
direction="vertical"
mainContent={
<>
<ListActionBar ... />
<ListTable ... />
</>
}
extendContent={
<ExtendInfoPanel
layout="horizontal"
sections={[
{
key: 'stats',
title: '数据统计',
content: <StatCards />
}
]}
/>
}
showExtend={showStats}
/>
</>
)
}
```
### 自定义尺寸和间距
```jsx
<SplitLayout
direction="horizontal"
mainContent={<div>主内容</div>}
extendContent={<div>扩展信息</div>}
extendSize={400}
gap={24}
/>
```
### 完整示例 - 虚拟机镜像页面
```jsx
import { useState } from 'react'
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ListActionBar from '../components/ListActionBar/ListActionBar'
import ListTable from '../components/ListTable/ListTable'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
import StatCard from '../components/StatCard/StatCard'
import ChartPanel from '../components/ChartPanel/ChartPanel'
function VirtualMachineImagePage() {
const [selectedRowKeys, setSelectedRowKeys] = useState([])
return (
<SplitLayout
direction="horizontal"
mainContent={
<>
<ListActionBar
actions={[
{ key: 'add', label: '新建镜像', type: 'primary' },
{ key: 'delete', label: '批量删除', danger: true },
]}
search={{ placeholder: '搜索镜像' }}
/>
<ListTable
columns={columns}
dataSource={data}
selectedRowKeys={selectedRowKeys}
onSelectionChange={setSelectedRowKeys}
/>
</>
}
extendContent={
<ExtendInfoPanel
sections={[
{
key: 'overview',
title: '概览',
content: (
<div style={{ display: 'grid', gap: '12px' }}>
<StatCard
key="total"
title="镜像总数"
value={32}
icon={<DatabaseOutlined />}
color="blue"
gridColumn="1 / -1"
/>
<StatCard
key="running"
title="运行中"
value={28}
color="green"
/>
<StatCard
key="stopped"
title="已停止"
value={4}
color="gray"
/>
</div>
),
},
{
key: 'charts',
title: '性能监控',
content: (
<>
<ChartPanel
type="line"
title="CPU使用率趋势"
data={cpuData}
height={180}
/>
<ChartPanel
type="line"
title="内存使用率趋势"
data={memoryData}
height={180}
/>
</>
),
},
]}
/>
}
extendSize={360}
extendPosition="right"
/>
)
}
```
## DOM 结构
### 横向布局
```html
<div class="split-layout split-layout-horizontal" style="gap: 16px">
<!-- 主内容区 -->
<div class="split-layout-main">
{mainContent}
</div>
<!-- 扩展信息区 -->
<div class="split-layout-extend split-layout-extend-right" style="width: 360px">
{extendContent}
</div>
</div>
```
### 纵向布局
```html
<div class="split-layout split-layout-vertical" style="gap: 16px">
<!-- 扩展信息区 -->
<div class="split-layout-extend split-layout-extend-top">
{extendContent}
</div>
<!-- 主内容区 -->
<div class="split-layout-main">
{mainContent}
</div>
</div>
```
## 响应式设计
### 横向布局响应式
| 屏幕宽度 | 布局行为 |
|---------|---------|
| ≥ 1200px | 显示扩展信息区 |
| < 1200px | |
```css
@media (max-width: 1200px) {
.split-layout-extend-right {
display: none;
}
}
```
### 纵向布局响应式
- 扩展区始终占满宽度
- 高度由内容自适应
- 通过 `showExtend` 参数控制显示/隐藏
## 样式定制
组件提供以下 CSS 类名供自定义样式:
- `.split-layout` - 布局容器
- `.split-layout-horizontal` - 横向布局模式
- `.split-layout-vertical` - 纵向布局模式
- `.split-layout-main` - 主内容区
- `.split-layout-extend` - 扩展信息区
- `.split-layout-extend-right` - 右侧扩展区(横向布局)
- `.split-layout-extend-top` - 顶部扩展区(纵向布局)
### 自定义样式示例
```css
/* 修改扩展区背景色 */
.split-layout-extend {
background: #f5f5f5;
}
/* 自定义滚动条样式(横向布局) */
.split-layout-extend-right::-webkit-scrollbar {
width: 8px;
}
.split-layout-extend-right::-webkit-scrollbar-thumb {
background: #1677ff;
border-radius: 4px;
}
```
## 使用场景
### 1. 横向布局场景
- **数据列表 + 信息面板**:左侧显示数据表格,右侧显示统计信息和图表
- **监控页面**:左侧显示设备列表,右侧显示实时监控数据
- **内容编辑 + 预览**:左侧编辑器,右侧实时预览
### 2. 纵向布局场景
- **带统计面板的列表页**:顶部显示统计卡片,下方显示数据列表
- **可展开的筛选区**:顶部显示筛选条件,下方显示筛选结果
- **概览信息页**:顶部显示关键指标,下方显示详细数据
## 注意事项
### 1. 横向布局
- **扩展区宽度建议**320-400px
- **主内容最小宽度**:确保至少 800px
- **总宽度建议**:≥ 1200px
- **扩展区滚动**:自动 sticky 定位,独立滚动
### 2. 纵向布局
- **扩展区高度**:由内容自适应,不需要设置固定高度
- **配合 PageTitleBar**:使用 toggle 功能控制显示/隐藏
- **内容组织**:避免扩展区内容过多,建议不超过 300px 高度
### 3. 内容组织
- **主内容区**:放置主要内容(列表、表格、表单等)
- **扩展区**:放置辅助信息(统计、图表、说明等)
- **避免**:扩展区放置过多交互元素
### 4. 性能考虑
- 扩展区内容会始终渲染(即使隐藏)
- 如需完全卸载,使用 `showExtend={false}`
- 大量图表建议使用懒加载
### 5. 布局选择
```jsx
// ✅ 适合横向布局
- 需要持续展示的监控信息
- 辅助信息较多且重要
- 页面宽度充足(> 1200px
// ✅ 适合纵向布局
- 统计信息可按需展开
- 移动端友好的布局
- 扩展内容简洁明了
// ❌ 不需要 SplitLayout
- 简单的列表页面
- 无扩展信息需求
- 直接使用 ListActionBar + ListTable
```
## 迁移指南
### 从旧版 API 迁移
**旧版代码**
```jsx
<SplitLayout
leftContent={<Content />}
rightContent={<SideInfoPanel sections={...} />}
rightWidth={360}
showRight={true}
/>
```
**新版代码**
```jsx
<SplitLayout
direction="horizontal"
mainContent={<Content />}
extendContent={<ExtendInfoPanel sections={...} />}
extendSize={360}
showExtend={true}
extendPosition="right"
/>
```
**变更对照表**
| 旧参数 | 新参数 | 说明 |
|--------|--------|------|
| leftContent | mainContent | 主内容区 |
| rightContent | extendContent | 扩展内容区 |
| rightWidth | extendSize | 扩展区尺寸 |
| showRight | showExtend | 显示扩展区 |
| - | direction | 新增:布局方向 |
| - | extendPosition | 新增:扩展区位置 |
## 配合使用的组件
- **ExtendInfoPanel** - 扩展信息面板容器(推荐)
- **StatCard** - 统计卡片
- **ChartPanel** - 图表展示
- **ListTable** - 列表表格
- **ListActionBar** - 列表操作栏
- **PageTitleBar** - 页面标题栏(配合纵向布局)
## 相关文档
- [主内容区布局](../layouts/content-area-layout.md) - 详细的布局使用指南
- [ExtendInfoPanel](./ExtendInfoPanel.md) - 扩展信息面板组件
- [StatCard](./StatCard.md) - 统计卡片组件
- [ChartPanel](./ChartPanel.md) - 图表面板组件

View File

@ -0,0 +1,298 @@
# StatCard 组件
## 组件说明
统计卡片组件,用于展示数值型统计数据。支持图标、颜色主题、趋势指示器等功能,适合在仪表盘或信息面板中使用。
## 组件位置
```
src/components/StatCard/StatCard.jsx
src/components/StatCard/StatCard.css
```
## 参数说明
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| title | string | 是 | - | 卡片标题 |
| value | number \| string | 是 | - | 统计值 |
| icon | ReactNode | 否 | - | 图标 |
| color | string | 否 | 'blue' | 主题颜色blue/green/orange/red/purple/gray 或自定义颜色值 |
| trend | TrendConfig | 否 | - | 趋势信息 |
| suffix | string | 否 | '' | 后缀单位 |
| layout | string | 否 | 'column' | 布局模式:'column'(一列)\| 'row'(两列) |
| className | string | 否 | '' | 自定义类名 |
### TrendConfig 配置项
| 属性名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| value | number | 是 | 趋势值(百分比,自动取绝对值) |
| direction | 'up' \| 'down' | 是 | 趋势方向 |
### 颜色预设
| 颜色名 | 色值 | 适用场景 |
|--------|------|---------|
| blue | #1677ff | 默认、总数、常规数据 |
| green | #52c41a | 成功、在线、正常状态 |
| orange | #faad14 | 警告、待处理 |
| red | #ff4d4f | 错误、危险、异常 |
| purple | #722ed1 | 特殊、高级功能 |
| gray | #8c8c8c | 禁用、离线、未激活 |
## 使用示例
### 基础用法
```jsx
import StatCard from '../components/StatCard/StatCard'
import { DatabaseOutlined } from '@ant-design/icons'
<StatCard
title="镜像总数"
value={32}
icon={<DatabaseOutlined />}
/>
```
### 带颜色主题
```jsx
<StatCard
title="在线用户"
value={156}
icon={<UserOutlined />}
color="green"
/>
<StatCard
title="错误数量"
value={3}
icon={<CloseCircleOutlined />}
color="red"
/>
```
### 带趋势指示
```jsx
<StatCard
title="访问量"
value={1250}
icon={<LineChartOutlined />}
trend={{ value: 12.5, direction: 'up' }}
/>
<StatCard
title="响应时间"
value={245}
suffix="ms"
trend={{ value: 8.3, direction: 'down' }}
color="green"
/>
```
### 自定义颜色
```jsx
<StatCard
title="自定义指标"
value={88}
color="#9c27b0"
suffix="%"
/>
```
### 布局模式
StatCard 支持两种布局模式,通过 `layout` 参数配置:
#### 一列布局(默认)
标题和图标在上方,数值和趋势在下方。适合垂直排列的卡片网格。
```jsx
<StatCard
title="CPU使用率"
value={45}
suffix="%"
icon={<DashboardOutlined />}
color="blue"
trend={{ value: 5, direction: 'up' }}
/>
```
#### 两列布局
左侧显示标题和图标,右侧显示数值和趋势。适合在较宽的容器中横向展示数据。
```jsx
<StatCard
layout="row"
title="内存使用率"
value={62}
suffix="%"
icon={<DatabaseOutlined />}
color="orange"
trend={{ value: 3, direction: 'down' }}
/>
```
#### 布局模式选择建议
- **一列布局column**
- 适用于窄宽度容器(如侧边信息面板)
- 适用于网格布局中垂直排列
- 数值作为视觉焦点,强调数据本身
- **两列布局row**
- 适用于较宽的容器(宽度 > 300px
- 适用于列表或卡片中横向展示
- 平衡展示标题和数值,节省垂直空间
### 网格布局
```jsx
<div style={{ display: 'grid', gap: '12px' }}>
<StatCard
title="总数"
value={100}
icon={<DatabaseOutlined />}
color="blue"
/>
<StatCard
title="在线"
value={85}
icon={<CheckCircleOutlined />}
color="green"
trend={{ value: 12, direction: 'up' }}
/>
<StatCard
title="离线"
value={15}
icon={<CloseCircleOutlined />}
color="gray"
/>
</div>
```
### 配合 SideInfoPanel 使用
```jsx
import SideInfoPanel from '../components/SideInfoPanel/SideInfoPanel'
import StatCard from '../components/StatCard/StatCard'
<SideInfoPanel
sections={[
{
key: 'overview',
title: '系统概览',
content: (
<div style={{ display: 'grid', gap: '12px' }}>
<StatCard
title="CPU使用率"
value={45}
suffix="%"
icon={<DashboardOutlined />}
trend={{ value: 5, direction: 'up' }}
/>
<StatCard
title="内存使用率"
value={62}
suffix="%"
color="orange"
trend={{ value: 3, direction: 'down' }}
/>
<StatCard
title="磁盘使用率"
value={78}
suffix="%"
color="red"
/>
</div>
),
},
]}
/>
```
## 样式定制
组件提供以下 CSS 类名供自定义样式:
- `.stat-card` - 卡片容器
- `.stat-card-column` - 一列布局模式
- `.stat-card-row` - 两列布局模式
- `.stat-card-header` - 卡片头部
- `.stat-card-title` - 标题文本
- `.stat-card-icon` - 图标
- `.stat-card-body` - 卡片内容
- `.stat-card-value` - 统计值
- `.stat-card-suffix` - 后缀单位
- `.stat-card-trend` - 趋势指示器
- `.trend-up` - 上升趋势
- `.trend-down` - 下降趋势
### 自定义样式示例
```css
/* 修改卡片圆角 */
.stat-card {
border-radius: 12px;
}
/* 自定义标题样式 */
.stat-card-title {
font-size: 14px;
font-weight: 600;
}
/* 自定义数值大小 */
.stat-card-value {
font-size: 28px;
}
```
## 使用场景
1. **仪表盘概览** - 展示关键业务指标
2. **系统监控** - 展示CPU、内存、磁盘使用率
3. **数据统计** - 展示用户数、订单数等统计数据
4. **性能指标** - 展示响应时间、吞吐量等性能数据
## 注意事项
1. **数值格式化**
- 大数值建议使用千分位格式(如 1,250
- 小数建议保留 1-2 位
- 使用 suffix 添加单位,而不是直接拼接在 value 中
2. **颜色使用**
- 保持颜色语义一致(绿色=正常,红色=错误)
- 避免过多颜色,建议同一页面不超过 4 种
- 优先使用预设颜色,保持风格统一
3. **趋势指示**
- 趋势值会自动取绝对值显示
- 上升趋势不一定是好事(如错误率上升)
- 根据业务含义选择合适的颜色
4. **布局建议**
- 一列布局column适合侧边信息面板和网格垂直排列
- 两列布局row适合较宽容器和横向展示
- 网格布局推荐使用:`display: grid; gap: 12px;`
- 避免卡片过宽,建议最大宽度 400px
- 根据容器宽度选择合适的 layout 模式
5. **图标选择**
- 使用与数据相关的图标
- 保持图标风格统一
- 避免过于复杂的图标
## 配合使用的组件
- **SideInfoPanel** - 侧边信息面板(推荐)
- **SplitLayout** - 分栏布局
- **ChartPanel** - 图表面板(配合使用)

View File

@ -0,0 +1,347 @@
# 主内容区布局
## 概述
主内容区布局是页面中除了导航栏外的核心内容区域,由 **SplitLayout****ExtendInfoPanel** 两个核心组件组成,提供灵活的横向(左右)和纵向(上下)分栏布局方案。
## 布局模式
### 1. 横向布局Horizontal
**适用场景**:需要在主内容区右侧展示扩展信息的页面
**布局结构**
```
┌─────────────────────────────────────────┐
│ PageTitleBar │
├──────────────────────────┬──────────────┤
│ │ │
│ Main Content │ Extend │
│ - ListActionBar │ Info │
│ - ListTable │ Panel │
│ │ │
└──────────────────────────┴──────────────┘
```
**代码示例**
```jsx
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
<SplitLayout
direction="horizontal"
mainContent={
<>
<ListActionBar ... />
<ListTable ... />
</>
}
extendContent={
<ExtendInfoPanel
sections={[
{
key: 'overview',
title: '概览',
icon: <DashboardOutlined />,
content: <StatCards />
},
{
key: 'charts',
title: '图表',
content: <Charts />
}
]}
/>
}
extendSize={360}
extendPosition="right"
/>
```
### 2. 纵向布局Vertical
**适用场景**:需要在主内容区顶部展示统计信息或扩展面板的页面
**布局结构**
```
┌─────────────────────────────────────────┐
│ PageTitleBar (带展开/收起按钮) │
├─────────────────────────────────────────┤
│ Extend Info Panel (可收起) │
│ - 统计卡片 / 图表 / 其他扩展信息 │
├─────────────────────────────────────────┤
│ Main Content │
│ - ListActionBar │
│ - ListTable │
│ │
└─────────────────────────────────────────┘
```
**代码示例**
```jsx
import { useState } from 'react'
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
function MyPage() {
const [showExtend, setShowExtend] = useState(false)
return (
<>
<PageTitleBar
title="页面标题"
showToggle={true}
defaultExpanded={false}
onToggle={setShowExtend}
/>
<SplitLayout
direction="vertical"
mainContent={
<>
<ListActionBar ... />
<ListTable ... />
</>
}
extendContent={
<ExtendInfoPanel
layout="horizontal"
sections={[
{
key: 'stats',
title: '数据统计',
content: (
<div style={{ display: 'flex', gap: '16px' }}>
<StatCard title="总数" value={100} />
<StatCard title="在线" value={80} />
</div>
)
}
]}
/>
}
showExtend={showExtend}
extendPosition="top"
/>
</>
)
}
```
## 核心组件
### SplitLayout
**职责**:主内容区的布局容器,支持横向和纵向分栏
**参数**
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| direction | string | 否 | 'horizontal' | 布局方向:'horizontal'(左右)\| 'vertical'(上下) |
| mainContent | ReactNode | 是 | - | 主内容区 |
| extendContent | ReactNode | 否 | - | 扩展内容区 |
| extendSize | number | 否 | 360 | 扩展区尺寸horizontal 模式下为宽度px |
| gap | number | 否 | 16 | 主内容与扩展区间距px |
| showExtend | boolean | 否 | true | 是否显示扩展区 |
| extendPosition | string | 否 | 'right'/'top' | 扩展区位置horizontal 模式下为 'right'vertical 模式下为 'top' |
| className | string | 否 | '' | 自定义类名 |
**详细文档**[SplitLayout.md](../components/SplitLayout.md)
### ExtendInfoPanel
**职责**:扩展信息面板容器,支持多个可折叠区块
**参数**
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| sections | Array | 是 | [] | 区块配置数组 |
| layout | string | 否 | 'vertical' | 布局方式:'vertical'(垂直堆叠)\| 'horizontal'(水平排列) |
| className | string | 否 | '' | 自定义类名 |
**Section 配置项**
| 属性名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| key | string | 是 | 区块唯一标识 |
| title | string | 是 | 区块标题 |
| icon | ReactNode | 否 | 标题图标 |
| content | ReactNode | 是 | 区块内容 |
| defaultCollapsed | boolean | 否 | 默认是否折叠 |
**详细文档**[ExtendInfoPanel.md](../components/ExtendInfoPanel.md)
## 使用场景
### 场景 1带右侧信息面板的列表页
**示例页面**虚拟机镜像管理页VirtualMachineImagePage
**特点**
- 左侧:操作栏 + 数据表格
- 右侧:概览统计 + 图表监控
**布局选择**横向布局horizontal
### 场景 2带顶部统计面板的列表页
**示例页面**用户列表页UserListPage
**特点**
- 顶部:可展开/收起的统计面板
- 下方:操作栏 + 数据表格
**布局选择**纵向布局vertical
### 场景 3纯表格列表页
**示例页面**:简单的数据列表页
**特点**
- 只有操作栏和数据表格
- 无扩展信息区
**布局选择**:直接使用 ListActionBar + ListTable不使用 SplitLayout
```jsx
<>
<PageTitleBar title="简单列表" />
<ListActionBar ... />
<ListTable ... />
</>
```
## 布局对比
| 特性 | 横向布局 | 纵向布局 | 无布局 |
|------|---------|---------|--------|
| 扩展区位置 | 右侧 | 顶部 | 无 |
| 扩展区尺寸 | 固定宽度 | 高度自适应 | - |
| 主内容宽度 | 自适应 | 100% | 100% |
| 展开/收起 | 响应式隐藏 | PageTitleBar 控制 | - |
| 适用场景 | 仪表盘、监控页 | 统计分析页 | 简单列表页 |
## 响应式设计
### 横向布局响应式
- **宽屏(> 1200px**:显示扩展信息区
- **中等屏幕(≤ 1200px**:自动隐藏扩展信息区,主内容占满
```css
@media (max-width: 1200px) {
.split-layout-extend-right {
display: none;
}
}
```
### 纵向布局响应式
- 扩展区始终占满宽度
- 通过 `showExtend` 参数控制显示/隐藏
- 建议配合 PageTitleBar 的 toggle 功能使用
## 最佳实践
### 1. 选择合适的布局模式
```jsx
// ✅ 好的做法:监控页面使用横向布局
<SplitLayout
direction="horizontal"
extendSize={360}
mainContent={<MonitorTable />}
extendContent={<RealTimeCharts />}
/>
// ✅ 好的做法:统计页面使用纵向布局
<SplitLayout
direction="vertical"
mainContent={<UserTable />}
extendContent={<StatsPanel />}
showExtend={showStats}
/>
// ❌ 避免:简单列表页使用复杂布局
// 直接使用 ListActionBar + ListTable 即可
```
### 2. ExtendInfoPanel 的 layout 选择
```jsx
// horizontal 方向的 SplitLayout 配合 vertical layout 的 ExtendInfoPanel
<SplitLayout
direction="horizontal"
extendContent={
<ExtendInfoPanel
layout="vertical" // 区块垂直堆叠
sections={[...]}
/>
}
/>
// vertical 方向的 SplitLayout 配合 horizontal layout 的 ExtendInfoPanel
<SplitLayout
direction="vertical"
extendContent={
<ExtendInfoPanel
layout="horizontal" // 区块水平排列
sections={[...]}
/>
}
/>
```
### 3. 合理设置扩展区尺寸
```jsx
// ✅ 横向布局:扩展区宽度推荐 320-400px
<SplitLayout
direction="horizontal"
extendSize={360}
/>
// ✅ 纵向布局:高度自适应,由内容决定
<SplitLayout
direction="vertical"
// 不需要设置 extendSize
/>
```
### 4. 统一命名规范
```jsx
// ✅ 使用新的参数命名
<SplitLayout
mainContent={...} // 主内容
extendContent={...} // 扩展内容
showExtend={...} // 显示扩展区
extendSize={...} // 扩展区尺寸
extendPosition={...} // 扩展区位置
/>
// ❌ 避免使用旧的命名(已废弃)
<SplitLayout
leftContent={...}
rightContent={...}
showRight={...}
rightWidth={...}
/>
```
## 相关组件
- [PageTitleBar](../components/PageTitleBar.md) - 页面标题栏
- [ListActionBar](../components/ListActionBar.md) - 列表操作栏
- [ListTable](../components/ListTable.md) - 列表表格
- [StatCard](../components/StatCard.md) - 统计卡片
- [ChartPanel](../components/ChartPanel.md) - 图表面板
- [InfoPanel](../components/InfoPanel.md) - 信息展示面板
## 示例页面
- **横向布局示例**`src/pages/VirtualMachineImagePage.jsx`
- **纵向布局示例**`src/pages/UserListPage.jsx`

26
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.0",
"echarts": "^6.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
@ -2003,6 +2004,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.244",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
@ -7379,6 +7390,12 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
@ -7999,6 +8016,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",

View File

@ -7,11 +7,13 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"clean": "bash scripts/clean.sh",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.0",
"echarts": "^6.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",

26
scripts/clean.sh 100755
View File

@ -0,0 +1,26 @@
#!/bin/bash
# 清理构建产物和临时文件
echo "清理构建产物..."
# 清理 dist 目录
if [ -d "dist" ]; then
rm -rf dist
echo "✓ 已清理 dist 目录"
fi
# 清理日志
if [ -d "logs" ]; then
rm -rf logs
echo "✓ 已清理 logs 目录"
fi
# 清理 node_modules可选取消注释使用
# if [ -d "node_modules" ]; then
# rm -rf node_modules
# echo "✓ 已清理 node_modules 目录"
# fi
echo ""
echo "清理完成!"

View File

@ -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 VirtualMachineImagePage from './pages/VirtualMachineImagePage'
import DocsPage from './pages/DocsPage'
function App() {
@ -14,7 +15,8 @@ function App() {
<Route path="/overview" element={<OverviewPage />} />
<Route path="/host/list" element={<HostListPage />} />
<Route path="/user/list" element={<UserListPage />} />
<Route path="/image/list" element={<ImageListPage />} />
<Route path="/image/system" element={<ImageListPage />} />
<Route path="/image/vm" element={<VirtualMachineImagePage />} />
<Route path="/design" element={<DocsPage />} />
{/* 其他路由将在后续添加 */}
</Routes>

View File

@ -0,0 +1,17 @@
/* 图表面板 */
.chart-panel {
margin-bottom: 16px;
}
.chart-panel:last-child {
margin-bottom: 0;
}
.chart-panel-title {
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
margin-bottom: 12px;
padding-left: 8px;
border-left: 3px solid #1677ff;
}

View File

@ -0,0 +1,202 @@
import { useEffect, useRef } from 'react'
import * as echarts from 'echarts'
import './ChartPanel.css'
/**
* 图表面板组件
* @param {Object} props
* @param {string} props.type - 图表类型: 'line' | 'bar' | 'pie' | 'ring'
* @param {string} props.title - 图表标题
* @param {Object} props.data - 图表数据
* @param {number} props.height - 图表高度默认 200px
* @param {Object} props.option - 自定义 ECharts 配置
* @param {string} props.className - 自定义类名
*/
function ChartPanel({ type = 'line', title, data, height = 200, option = {}, className = '' }) {
const chartRef = useRef(null)
const chartInstance = useRef(null)
useEffect(() => {
if (!chartRef.current || !data) return
// 使 setTimeout DOM
const timer = setTimeout(() => {
//
if (!chartInstance.current) {
chartInstance.current = echarts.init(chartRef.current)
}
//
const chartOption = getChartOption(type, data, option)
chartInstance.current.setOption(chartOption, true)
}, 0)
// 使 passive
const handleResize = () => {
if (chartInstance.current) {
chartInstance.current.resize()
}
}
//
window.addEventListener('resize', handleResize, { passive: true })
return () => {
clearTimeout(timer)
window.removeEventListener('resize', handleResize)
}
}, [type, data, option])
//
useEffect(() => {
return () => {
chartInstance.current?.dispose()
}
}, [])
return (
<div className={`chart-panel ${className}`}>
{title && <div className="chart-panel-title">{title}</div>}
<div ref={chartRef} style={{ width: '100%', height: `${height}px` }} />
</div>
)
}
/**
* 根据图表类型生成 ECharts 配置
*/
function getChartOption(type, data, customOption) {
const baseOption = {
grid: {
left: '10%',
right: '5%',
top: '15%',
bottom: '15%',
},
tooltip: {
trigger: type === 'pie' || type === 'ring' ? 'item' : 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#e8e8e8',
borderWidth: 1,
textStyle: {
color: '#333',
},
},
}
switch (type) {
case 'line':
return {
...baseOption,
xAxis: {
type: 'category',
data: data.xAxis || [],
boundaryGap: false,
axisLine: { lineStyle: { color: '#e8e8e8' } },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#e8e8e8' } },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
splitLine: { lineStyle: { color: '#f0f0f0' } },
},
series: [
{
type: 'line',
data: data.series || [],
smooth: true,
lineStyle: { width: 2, color: '#1677ff' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(22, 119, 255, 0.3)' },
{ offset: 1, color: 'rgba(22, 119, 255, 0.05)' },
]),
},
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#1677ff' },
},
],
...customOption,
}
case 'bar':
return {
...baseOption,
xAxis: {
type: 'category',
data: data.xAxis || [],
axisLine: { lineStyle: { color: '#e8e8e8' } },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#e8e8e8' } },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
splitLine: { lineStyle: { color: '#f0f0f0' } },
},
series: [
{
type: 'bar',
data: data.series || [],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4096ff' },
{ offset: 1, color: '#1677ff' },
]),
borderRadius: [4, 4, 0, 0],
},
barWidth: '50%',
},
],
...customOption,
}
case 'pie':
case 'ring':
return {
...baseOption,
grid: undefined,
legend: {
orient: 'vertical',
right: '10%',
top: 'center',
textStyle: { color: '#8c8c8c', fontSize: 12 },
},
series: [
{
type: 'pie',
radius: type === 'ring' ? ['40%', '65%'] : '65%',
center: ['40%', '50%'],
data: data.series || [],
label: {
fontSize: 11,
color: '#8c8c8c',
},
labelLine: {
lineStyle: { color: '#d9d9d9' },
},
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2,
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.3)',
},
},
},
],
...customOption,
}
default:
return { ...baseOption, ...customOption }
}
}
export default ChartPanel

View File

@ -10,7 +10,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
padding: 16px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
@ -65,13 +65,13 @@
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 24px;
}
/* 标签页区域 */
.detail-drawer-tabs {
background: #ffffff;
padding-top: 16px;
padding-left: 12px;
padding: 0;
min-height: 400px;
}
@ -85,8 +85,7 @@
.detail-drawer-tabs :global(.ant-tabs-nav) {
padding: 0;
margin: 0 24px;
margin-bottom: 0;
margin: 0 0 16px 0;
background: transparent;
}
@ -115,6 +114,6 @@
}
.detail-drawer-tab-content {
padding: 24px;
padding: 0;
background: #ffffff;
}

View File

@ -0,0 +1,105 @@
/* 扩展信息面板容器 */
.extend-info-panel {
display: flex;
gap: 16px;
width: 100%;
}
/* 垂直布局(默认) */
.extend-info-panel-vertical {
flex-direction: column;
}
/* 水平布局 */
.extend-info-panel-horizontal {
flex-direction: row;
flex-wrap: wrap;
}
/* 信息区块 */
.extend-info-section {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: all 0.3s ease;
}
/* 水平布局时区块自适应宽度 */
.extend-info-panel-horizontal .extend-info-section {
flex: 1;
min-width: 0;
}
.extend-info-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 区块头部 */
.extend-info-section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
border-bottom: 1px solid #e8e8e8;
cursor: pointer;
user-select: none;
transition: background 0.2s ease;
}
.extend-info-section-header:hover {
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%);
}
.extend-info-section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
}
.extend-info-section-icon {
display: flex;
align-items: center;
font-size: 16px;
color: #1677ff;
}
.extend-info-section-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
background: transparent;
color: #8c8c8c;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
}
.extend-info-section-toggle:hover {
background: rgba(0, 0, 0, 0.06);
color: #1677ff;
}
/* 区块内容 */
.extend-info-section-content {
padding: 16px 20px;
animation: expandContent 0.3s ease-out;
}
@keyframes expandContent {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -0,0 +1,68 @@
import { useState } from 'react'
import { UpOutlined, DownOutlined } from '@ant-design/icons'
import './ExtendInfoPanel.css'
/**
* 扩展信息面板组件
* @param {Object} props
* @param {Array} props.sections - 信息区块配置数组
* @param {string} props.sections[].key - 区块唯一键
* @param {string} props.sections[].title - 区块标题
* @param {ReactNode} props.sections[].icon - 标题图标
* @param {ReactNode} props.sections[].content - 区块内容
* @param {boolean} props.sections[].defaultCollapsed - 默认是否折叠
* @param {boolean} props.sections[].hideTitleBar - 是否隐藏该区块的标题栏默认 false
* @param {string} props.layout - 布局方式'vertical'垂直堆叠| 'horizontal'水平排列
* @param {string} props.className - 自定义类名
*/
function ExtendInfoPanel({ sections = [], layout = 'vertical', className = '' }) {
const [collapsedSections, setCollapsedSections] = useState(() => {
const initial = {}
sections.forEach((section) => {
if (section.defaultCollapsed) {
initial[section.key] = true
}
})
return initial
})
const toggleSection = (key) => {
setCollapsedSections((prev) => ({
...prev,
[key]: !prev[key],
}))
}
return (
<div className={`extend-info-panel extend-info-panel-${layout} ${className}`}>
{sections.map((section) => {
const isCollapsed = collapsedSections[section.key]
const hideTitleBar = section.hideTitleBar === true
return (
<div key={section.key} className="extend-info-section">
{/* 区块头部 - 可配置隐藏 */}
{!hideTitleBar && (
<div className="extend-info-section-header" onClick={() => toggleSection(section.key)}>
<div className="extend-info-section-title">
{section.icon && <span className="extend-info-section-icon">{section.icon}</span>}
<span>{section.title}</span>
</div>
<button className="extend-info-section-toggle" type="button">
{isCollapsed ? <DownOutlined /> : <UpOutlined />}
</button>
</div>
)}
{/* 区块内容 - 如果隐藏标题栏则总是显示,否则根据折叠状态 */}
{(hideTitleBar || !isCollapsed) && (
<div className="extend-info-section-content">{section.content}</div>
)}
</div>
)
})}
</div>
)
}
export default ExtendInfoPanel

View File

@ -1,12 +1,12 @@
/* 信息面板 */
.info-panel {
padding: 6px 8px;
padding: 0;
background: #ffffff;
}
/* 信息区域容器 */
.info-panel > :global(.ant-row) {
padding: 32px;
padding: 24px;
background: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
@ -25,17 +25,18 @@
border-bottom: none;
}
/* 添加底部装饰条 */
/* 添加左侧装饰条 */
.info-panel-item::before {
content: '';
position: absolute;
left: 3px;
bottom: -1px;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 3px;
background: linear-gradient(90deg, #1677ff 0%, #4096ff 100%);
height: 0;
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
border-radius: 2px;
transition: width 0.3s ease;
transition: all 0.3s ease;
}
.info-panel-item:hover {
@ -49,7 +50,8 @@
}
.info-panel-item:hover::before {
width: 60px;
width: 3px;
height: 60%;
}
.info-panel-label {

View File

@ -5,11 +5,12 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin-bottom: 4px;
padding: 16px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
width: 100%;
}
.list-action-bar-left,

View File

@ -4,40 +4,7 @@
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;
height: 626px;
overflow-y: auto;
width: 100%;
}

View File

@ -28,7 +28,7 @@ function ListTable({
showQuickJumper: true,
showTotal: (total) => `${total}`,
},
scroll = { x: 1200 },
scroll = { x: 1200},
onRowClick,
selectedRow,
loading = false,
@ -45,6 +45,7 @@ function ListTable({
return (
<div className={`list-table-container ${className}`}>
<Table
size="middle"
rowSelection={rowSelection}
columns={columns}
dataSource={dataSource}

View File

@ -10,7 +10,7 @@ import {
CustomerServiceOutlined,
UserOutlined,
} from '@ant-design/icons'
import headerMenuData from '../../constants/headerMenuData.json'
import headerMenuData from '../../data/headerMenuData.json'
import logoFull from '../../assets/logo-full.png'
import './AppHeader.css'

View File

@ -9,7 +9,7 @@ import {
AppstoreOutlined,
SettingOutlined,
} from '@ant-design/icons'
import menuData from '../../constants/menuData.json'
import menuData from '../../data/menuData.json'
import './AppSider.css'
const { Sider } = Layout

View File

@ -19,6 +19,6 @@
}
.content-wrapper {
padding: 24px;
padding: 16px;
min-height: 100%;
}

View File

@ -132,6 +132,28 @@
transform: translateY(-1px);
}
/* 扩展内容区域 */
.title-bar-expanded-content {
position: relative;
z-index: 1;
margin-top: 8px;
padding: 8px;
background: #ffffff;
border: 1px solid rgba(139, 92, 246, 0.1);
animation: expandContent 0.3s ease-out;
}
@keyframes expandContent {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式适配 */
@media (max-width: 768px) {
.title-bar-content {

View File

@ -9,7 +9,7 @@ function PageTitleBar({
actions,
showToggle = false,
onToggle,
defaultExpanded = true,
defaultExpanded = false,
}) {
const [expanded, setExpanded] = useState(defaultExpanded)

View File

@ -0,0 +1,88 @@
/* 侧边信息面板容器 */
.side-info-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 信息区块 */
.side-info-section {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: all 0.3s ease;
}
.side-info-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 区块头部 */
.side-info-section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
border-bottom: 1px solid #e8e8e8;
cursor: pointer;
user-select: none;
transition: background 0.2s ease;
}
.side-info-section-header:hover {
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%);
}
.side-info-section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
}
.side-info-section-icon {
display: flex;
align-items: center;
font-size: 16px;
color: #1677ff;
}
.side-info-section-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
background: transparent;
color: #8c8c8c;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
}
.side-info-section-toggle:hover {
background: rgba(0, 0, 0, 0.06);
color: #1677ff;
}
/* 区块内容 */
.side-info-section-content {
padding: 16px 20px;
animation: expandContent 0.3s ease-out;
}
@keyframes expandContent {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -0,0 +1,58 @@
import { useState } from 'react'
import { UpOutlined, DownOutlined } from '@ant-design/icons'
import './SideInfoPanel.css'
/**
* 侧边信息面板组件
* @param {Object} props
* @param {Array} props.sections - 信息区块配置数组
* @param {string} props.className - 自定义类名
*/
function SideInfoPanel({ sections = [], className = '' }) {
const [collapsedSections, setCollapsedSections] = useState(() => {
const initial = {}
sections.forEach((section) => {
if (section.defaultCollapsed) {
initial[section.key] = true
}
})
return initial
})
const toggleSection = (key) => {
setCollapsedSections((prev) => ({
...prev,
[key]: !prev[key],
}))
}
return (
<div className={`side-info-panel ${className}`}>
{sections.map((section) => {
const isCollapsed = collapsedSections[section.key]
return (
<div key={section.key} className="side-info-section">
{/* 区块头部 */}
<div className="side-info-section-header" onClick={() => toggleSection(section.key)}>
<div className="side-info-section-title">
{section.icon && <span className="side-info-section-icon">{section.icon}</span>}
<span>{section.title}</span>
</div>
<button className="side-info-section-toggle" type="button">
{isCollapsed ? <DownOutlined /> : <UpOutlined />}
</button>
</div>
{/* 区块内容 */}
{!isCollapsed && (
<div className="side-info-section-content">{section.content}</div>
)}
</div>
)
})}
</div>
)
}
export default SideInfoPanel

View File

@ -0,0 +1,72 @@
/* 分栏布局容器 */
.split-layout {
display: flex;
width: 100%;
align-items: flex-start;
}
/* 横向布局(左右分栏) */
.split-layout-horizontal {
flex-direction: row;
}
/* 纵向布局(上下分栏) */
.split-layout-vertical {
flex-direction: column;
}
/* 主内容区 */
.split-layout-main {
flex: 1;
min-width: 0;
width: 100%;
display: flex;
flex-direction: column;
}
/* 扩展信息区 */
.split-layout-extend {
flex-shrink: 0;
background: #ffffff;
}
/* 右侧扩展区(横向布局) */
.split-layout-extend-right {
height: 693px;
overflow-y: auto;
overflow-x: hidden;
position: sticky;
top: 16px;
padding-right: 4px;
}
/* 顶部扩展区(纵向布局) */
.split-layout-extend-top {
width: 100%;
}
/* 滚动条样式(横向布局右侧扩展区) */
.split-layout-extend-right::-webkit-scrollbar {
width: 6px;
}
.split-layout-extend-right::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 3px;
}
.split-layout-extend-right::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
}
.split-layout-extend-right::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
}
/* 响应式:小屏幕时隐藏右侧扩展区 */
@media (max-width: 1200px) {
.split-layout-extend-right {
display: none;
}
}

View File

@ -0,0 +1,69 @@
import './SplitLayout.css'
/**
* 主内容区布局组件
* @param {Object} props
* @param {string} props.direction - 布局方向'horizontal'左右| 'vertical'上下
* @param {ReactNode} props.mainContent - 主内容区
* @param {ReactNode} props.extendContent - 扩展内容区
* @param {number} props.extendSize - 扩展区尺寸horizontal 模式下为宽度px
* @param {number} props.gap - 主内容与扩展区间距px
* @param {boolean} props.showExtend - 是否显示扩展区
* @param {string} props.extendPosition - 扩展区位置horizontal: 'right', vertical: 'top'
* @param {string} props.className - 自定义类名
*
* @deprecated 旧参数向后兼容leftContent, rightContent, rightWidth, showRight
*/
function SplitLayout({
// API
direction = 'horizontal',
mainContent,
extendContent,
extendSize = 360,
gap = 16,
showExtend = true,
extendPosition,
className = '',
// API
leftContent,
rightContent,
rightWidth,
showRight,
}) {
// 使 API API
const actualMainContent = mainContent || leftContent
const actualExtendContent = extendContent || rightContent
const actualExtendSize = extendSize !== 360 ? extendSize : (rightWidth || 360)
const actualShowExtend = showExtend !== undefined ? showExtend : (showRight !== undefined ? showRight : true)
const actualDirection = direction
const actualExtendPosition = extendPosition || (actualDirection === 'horizontal' ? 'right' : 'top')
return (
<div
className={`split-layout split-layout-${actualDirection} ${className}`}
style={{ gap: `${gap}px` }}
>
{/* 纵向布局且扩展区在顶部时,先渲染扩展区 */}
{actualDirection === 'vertical' && actualExtendPosition === 'top' && actualShowExtend && actualExtendContent && (
<div className="split-layout-extend split-layout-extend-top">
{actualExtendContent}
</div>
)}
{/* 主内容区 */}
<div className="split-layout-main">{actualMainContent}</div>
{/* 横向布局时,扩展区在右侧 */}
{actualDirection === 'horizontal' && actualShowExtend && actualExtendContent && (
<div
className="split-layout-extend split-layout-extend-right"
style={{ width: `${actualExtendSize}px` }}
>
{actualExtendContent}
</div>
)}
</div>
)
}
export default SplitLayout

View File

@ -0,0 +1,108 @@
/* 统计卡片 */
.stat-card {
padding: 16px;
background: #ffffff;
border-radius: 8px;
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.stat-card:hover {
border-color: #d9d9d9;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 一列布局(默认) */
.stat-card-column {
/* 继承默认样式 */
}
/* 两列布局 */
.stat-card.stat-card-row {
display: flex;
align-items: center;
gap: 16px;
}
.stat-card.stat-card-row .stat-card-header {
flex: 1;
margin-bottom: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.stat-card.stat-card-row .stat-card-body {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
/* 卡片头部 */
.stat-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.stat-card-title {
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
font-weight: 500;
}
.stat-card-icon {
font-size: 18px;
display: flex;
align-items: center;
}
/* 卡片内容 */
.stat-card-body {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 8px;
}
.stat-card-value {
font-size: 24px;
font-weight: 600;
line-height: 1;
}
.stat-card-suffix {
font-size: 14px;
font-weight: 400;
margin-left: 4px;
color: rgba(0, 0, 0, 0.45);
}
/* 趋势指示器 */
.stat-card-trend {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 500;
padding: 2px 6px;
border-radius: 4px;
}
.stat-card-trend.trend-up {
color: #52c41a;
background: #f6ffed;
}
.stat-card-trend.trend-down {
color: #ff4d4f;
background: #fff1f0;
}
.stat-card-trend svg {
font-size: 10px;
}

View File

@ -0,0 +1,78 @@
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
import './StatCard.css'
/**
* 统计卡片组件
* @param {Object} props
* @param {string} props.title - 卡片标题
* @param {number|string} props.value - 统计值
* @param {ReactNode} props.icon - 图标
* @param {string} props.color - 主题颜色默认 'blue'
* @param {Object} props.trend - 趋势信息 { value: number, direction: 'up' | 'down' }
* @param {string} props.suffix - 后缀单位
* @param {string} props.layout - 布局模式: 'column' | 'row'默认 'column'一列
* @param {string} props.gridColumn - 网格列跨度 '1 / -1' 表示占满整行
* @param {string} props.className - 自定义类名
* @param {Function} props.onClick - 点击事件处理函数
* @param {Object} props.style - 自定义样式对象
*/
function StatCard({
title,
value,
icon,
color = 'blue',
trend,
suffix = '',
layout = 'column',
gridColumn,
className = '',
onClick,
style: customStyle = {},
}) {
const colorMap = {
blue: '#1677ff',
green: '#52c41a',
orange: '#faad14',
red: '#ff4d4f',
purple: '#722ed1',
gray: '#8c8c8c',
}
const themeColor = colorMap[color] || color
const style = {
...(gridColumn ? { gridColumn } : {}),
...customStyle,
}
return (
<div className={`stat-card stat-card-${layout} ${className}`} style={style} onClick={onClick}>
<div className="stat-card-header">
<span className="stat-card-title">{title}</span>
{icon && (
<span className="stat-card-icon" style={{ color: themeColor }}>
{icon}
</span>
)}
</div>
<div className="stat-card-body">
<div className="stat-card-value" style={{ color: themeColor }}>
{value}
{suffix && <span className="stat-card-suffix">{suffix}</span>}
</div>
{trend && (
<div
className={`stat-card-trend ${trend.direction === 'up' ? 'trend-up' : 'trend-down'}`}
>
{trend.direction === 'up' ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
<span>{Math.abs(trend.value)}%</span>
</div>
)}
</div>
</div>
)
}
export default StatCard

View File

@ -10,6 +10,22 @@
}
]
},
{
"key": "layouts",
"label": "布局文档",
"children": [
{
"key": "main-layout",
"label": "主布局",
"path": "/docs/layouts/main-layout.md"
},
{
"key": "content-area-layout",
"label": "主内容区布局",
"path": "/docs/layouts/content-area-layout.md"
}
]
},
{
"key": "components",
"label": "组件文档",
@ -58,17 +74,26 @@
"key": "toast",
"label": "Toast",
"path": "/docs/components/Toast.md"
}
]
},
{
"key": "pages",
"label": "页面文档",
"children": [
},
{
"key": "main-layout",
"label": "主布局",
"path": "/docs/pages/main-layout.md"
"key": "split-layout",
"label": "SplitLayout",
"path": "/docs/components/SplitLayout.md"
},
{
"key": "extend-info-panel",
"label": "ExtendInfoPanel",
"path": "/docs/components/ExtendInfoPanel.md"
},
{
"key": "stat-card",
"label": "StatCard",
"path": "/docs/components/StatCard.md"
},
{
"key": "chart-panel",
"label": "ChartPanel",
"path": "/docs/components/ChartPanel.md"
}
]
}

View File

@ -89,6 +89,54 @@
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-2503-HWE-Release-2503-X86_64.iso",
"btPath": "--",
"description": "--"
},
{
"id": 8,
"name": "Kylin-Desktop-V10-SP1-HWE-Release-2303-X86_64",
"os": "linux",
"version": "2303",
"status": "成功",
"uploadTime": "2025-10-09 09:55:41",
"fileName": "Kylin-Desktop-V10-SP1-HWE-Release-2303-X86_64.iso",
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-HWE-Release-2303-X86_64.iso",
"btPath": "--",
"description": "--"
},
{
"id": 9,
"name": "ky2203",
"os": "linux",
"version": "2203",
"status": "成功",
"uploadTime": "2025-10-13 09:41:50",
"fileName": "ky2203.iso",
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-General-Release-2203-X86_64.iso",
"btPath": "--",
"description": "--"
},
{
"id": 10,
"name": "windows-10",
"os": "windows",
"version": "1",
"status": "成功",
"uploadTime": "2025-10-14 16:54:38",
"fileName": "windows-10.iso",
"filePath": "/vms/iso/cn_windows_10_business.iso",
"btPath": "--",
"description": "--"
},
{
"id": 11,
"name": "windows-11",
"os": "windows",
"version": "1",
"status": "成功",
"uploadTime": "2025-10-14 16:54:49",
"fileName": "windows-11.iso",
"filePath": "/vms/iso/Windows11.iso",
"btPath": "--",
"description": "--"
}
]
}

View File

@ -74,7 +74,7 @@
{
"key": "image-system",
"label": "系统镜像",
"path": "/image/list"
"path": "/image/system"
},
{
"key": "image-vm",

View File

@ -0,0 +1,24 @@
{
"images": [
{
"id": 1,
"name": "Windows Server 2022 Standard",
"os": "Windows",
"version": "21H2",
"status": "running",
"cpuUsage": 45,
"memoryUsage": 62,
"createTime": "2024-01-15 10:30:00"
},
{
"id": 2,
"name": "Ubuntu Server 22.04 LTS",
"os": "Linux",
"version": "22.04",
"status": "running",
"cpuUsage": 28,
"memoryUsage": 45,
"createTime": "2024-02-10 14:20:00"
}
]
}

View File

@ -6,87 +6,11 @@ import remarkGfm from 'remark-gfm'
import rehypeRaw from 'rehype-raw'
import rehypeHighlight from 'rehype-highlight'
import 'highlight.js/styles/github.css'
import docsMenuData from '../data/docsMenuData.json'
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('')

View File

@ -2,6 +2,25 @@
width: 100%;
}
/* 统计卡片网格 */
.stat-cards-grid {
display: grid;
gap: 12px;
}
/* 不同列数的网格布局 */
.stat-cards-grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.stat-cards-grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.stat-cards-grid-4 {
grid-template-columns: repeat(4, 1fr);
}
/* 统计面板 - HostListPage 特有 */
.stats-panel {
margin-bottom: 16px;
@ -179,14 +198,20 @@
/* 响应式 */
@media (max-width: 1200px) {
.stats-panel :global(.ant-row) {
display: grid;
.stat-cards-grid-4 {
grid-template-columns: repeat(2, 1fr);
}
.stat-cards-grid-3 {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.stats-panel :global(.ant-row) {
.stat-cards-grid,
.stat-cards-grid-2,
.stat-cards-grid-3,
.stat-cards-grid-4 {
grid-template-columns: 1fr;
}
}

View File

@ -14,7 +14,6 @@ import {
Card,
Row,
Col,
Statistic,
TreeSelect,
} from 'antd'
import {
@ -30,6 +29,7 @@ import {
DesktopOutlined,
DatabaseOutlined,
UserOutlined,
DashboardOutlined,
} from '@ant-design/icons'
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
import ListActionBar from '../components/ListActionBar/ListActionBar'
@ -37,6 +37,9 @@ 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 SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
import StatCard from '../components/StatCard/StatCard'
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
import Toast from '../components/Toast/Toast'
import hostData from '../data/hostData.json'
@ -54,7 +57,7 @@ function HostListPage() {
const [selectedGroupName, setSelectedGroupName] = useState('')
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
const [filteredHosts, setFilteredHosts] = useState(hostData.hosts)
const [showStatsPanel, setShowStatsPanel] = useState(true)
const [showStatsPanel, setShowStatsPanel] = useState(false)
const [statusFilter, setStatusFilter] = useState(null)
//
@ -613,153 +616,168 @@ function HostListPage() {
title="主机列表"
description="查看和管理所有接入的主机终端,包括服务器和办公设备"
showToggle={true}
defaultExpanded={false}
onToggle={(expanded) => setShowStatsPanel(expanded)}
/>
{/* 数据统计面板 */}
{showStatsPanel && (
<div className="stats-panel">
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card
className={`stat-card-small ${statusFilter === null ? '' : 'stat-card-dimmed'}`}
hoverable
onClick={handleTotalClick}
style={{ cursor: 'pointer' }}
>
<Statistic
title="总主机数"
value={hostData.hosts.length}
prefix={<DesktopOutlined />}
valueStyle={{ color: '#1677ff' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card
className={`stat-card-small ${statusFilter === 'online' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
hoverable
onClick={() => handleStatusFilterClick('online')}
style={{ cursor: 'pointer' }}
>
<Statistic
title="在线主机"
value={hostData.hosts.filter((h) => h.status === 'online').length}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card
className={`stat-card-small ${statusFilter === 'offline' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
hoverable
onClick={() => handleStatusFilterClick('offline')}
style={{ cursor: 'pointer' }}
>
<Statistic
title="离线主机"
value={hostData.hosts.filter((h) => h.status === 'offline').length}
prefix={<CloseCircleOutlined />}
valueStyle={{ color: '#8c8c8c' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card className="stat-card-small stat-card-result">
<Statistic
title="筛选结果"
value={filteredHosts.length}
prefix={<SearchOutlined />}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
</Row>
</div>
)}
{/* 操作栏 - 使用新组件 */}
<ListActionBar
actions={[
{
key: 'add',
label: '新增主机',
icon: <PlusOutlined />,
type: 'primary',
onClick: () => {
setEditMode('add')
setSelectedHost(null)
setShowDetailDrawer(false)
setShowEditDrawer(true)
},
},
{
key: 'batchPowerOn',
label: '批量开机',
icon: <PoweroffOutlined />,
disabled: selectedRowKeys.length === 0,
onClick: () => console.log('批量开机'),
},
{
key: 'batchPowerOff',
label: '批量关机',
icon: <PoweroffOutlined />,
disabled: selectedRowKeys.length === 0,
onClick: () => console.log('批量关机'),
},
{
key: 'batchDelete',
label: '批量删除',
icon: <DeleteOutlined />,
danger: true,
disabled: selectedRowKeys.length === 0,
onClick: handleBatchDelete,
},
]}
search={{
placeholder: '搜索主机名、IP或MAC地址',
value: searchKeyword,
onSearch: handleSearch,
onChange: handleSearch,
}}
filter={{
content: (
<TreeFilterPanel
treeData={treeData}
selectedKey={selectedGroup}
tempSelectedKey={tempSelectedGroup}
treeTitle="终端分组"
onSelect={setTempSelectedGroup}
onConfirm={handleConfirmFilter}
onClear={handleClearFilter}
placeholder="请选择终端分组进行筛选"
{/* 使用 SplitLayout 纵向布局 */}
<SplitLayout
direction="vertical"
mainContent={
<>
{/* 操作栏 - 使用新组件 */}
<ListActionBar
actions={[
{
key: 'add',
label: '新增主机',
icon: <PlusOutlined />,
type: 'primary',
onClick: () => {
setEditMode('add')
setSelectedHost(null)
setShowDetailDrawer(false)
setShowEditDrawer(true)
},
},
{
key: 'batchPowerOn',
label: '批量开机',
icon: <PoweroffOutlined />,
disabled: selectedRowKeys.length === 0,
onClick: () => console.log('批量开机'),
},
{
key: 'batchPowerOff',
label: '批量关机',
icon: <PoweroffOutlined />,
disabled: selectedRowKeys.length === 0,
onClick: () => console.log('批量关机'),
},
{
key: 'batchDelete',
label: '批量删除',
icon: <DeleteOutlined />,
danger: true,
disabled: selectedRowKeys.length === 0,
onClick: handleBatchDelete,
},
]}
search={{
placeholder: '搜索主机名、IP或MAC地址',
value: searchKeyword,
onSearch: handleSearch,
onChange: 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,
}}
showRefresh
onRefresh={() => console.log('刷新')}
/>
),
title: '高级筛选',
visible: showFilterPopover,
onVisibleChange: (visible) => {
setShowFilterPopover(visible)
if (visible) {
setTempSelectedGroup(selectedGroup)
}
},
selectedLabel: selectedGroupName,
isActive: !!selectedGroup,
}}
showRefresh
onRefresh={() => console.log('刷新')}
/>
{/* 数据表格 - 使用新组件 */}
<ListTable
columns={columns}
dataSource={filteredHosts}
selectedRowKeys={selectedRowKeys}
onSelectionChange={setSelectedRowKeys}
onRowClick={handleRowClick}
selectedRow={selectedHost}
scroll={{ x: 1600 }}
{/* 数据表格 - 使用新组件 */}
<ListTable
columns={columns}
dataSource={filteredHosts}
selectedRowKeys={selectedRowKeys}
onSelectionChange={setSelectedRowKeys}
onRowClick={handleRowClick}
selectedRow={selectedHost}
scroll={{ x: 1600 }}
/>
</>
}
extendContent={
<ExtendInfoPanel
layout="horizontal"
sections={[
{
key: 'stats',
title: '数据统计',
icon: <DashboardOutlined />,
defaultCollapsed: false,
hideTitleBar: true,
content: (
<div className="stat-cards-grid stat-cards-grid-4">
<StatCard
key="total"
title="总主机数"
value={hostData.hosts.length}
icon={<DesktopOutlined />}
color="blue"
className={statusFilter === null ? '' : 'stat-card-dimmed'}
onClick={handleTotalClick}
style={{ cursor: 'pointer' }}
/>
<StatCard
key="online"
title="在线主机"
value={hostData.hosts.filter((h) => h.status === 'online').length}
icon={<CheckCircleOutlined />}
color="green"
className={
statusFilter === 'online'
? 'stat-card-active'
: statusFilter !== null
? 'stat-card-dimmed'
: ''
}
onClick={() => handleStatusFilterClick('online')}
style={{ cursor: 'pointer' }}
/>
<StatCard
key="offline"
title="离线主机"
value={hostData.hosts.filter((h) => h.status === 'offline').length}
icon={<CloseCircleOutlined />}
color="gray"
className={
statusFilter === 'offline'
? 'stat-card-active'
: statusFilter !== null
? 'stat-card-dimmed'
: ''
}
onClick={() => handleStatusFilterClick('offline')}
style={{ cursor: 'pointer' }}
/>
<StatCard
key="filtered"
title="筛选结果"
value={filteredHosts.length}
icon={<SearchOutlined />}
color="orange"
/>
</div>
),
},
]}
/>
}
showExtend={showStatsPanel}
extendPosition="top"
/>
{/* 详情抽屉 - 使用新组件 */}

View File

@ -2,6 +2,25 @@
width: 100%;
}
/* 统计卡片网格 */
.stat-cards-grid {
display: grid;
gap: 12px;
}
/* 不同列数的网格布局 */
.stat-cards-grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.stat-cards-grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.stat-cards-grid-4 {
grid-template-columns: repeat(4, 1fr);
}
/* 统计面板 - UserListPage 特有 */
.stats-panel {
margin-bottom: 16px;
@ -143,14 +162,20 @@
/* 响应式 */
@media (max-width: 1200px) {
.stats-panel :global(.ant-row) {
display: grid;
.stat-cards-grid-4 {
grid-template-columns: repeat(2, 1fr);
}
.stat-cards-grid-3 {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.stats-panel :global(.ant-row) {
.stat-cards-grid,
.stat-cards-grid-2,
.stat-cards-grid-3,
.stat-cards-grid-4 {
grid-template-columns: 1fr;
}
}

View File

@ -7,14 +7,13 @@ import {
Select,
Input,
Divider,
Card,
Row,
Col,
Statistic,
Switch,
Badge,
Drawer,
TreeSelect,
Card,
Row,
Col,
} from 'antd'
import {
UserOutlined,
@ -28,6 +27,7 @@ import {
CloseCircleOutlined,
DesktopOutlined,
DatabaseOutlined,
DashboardOutlined,
} from '@ant-design/icons'
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
import ListActionBar from '../components/ListActionBar/ListActionBar'
@ -35,6 +35,9 @@ 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 SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
import StatCard from '../components/StatCard/StatCard'
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
import Toast from '../components/Toast/Toast'
import userData from '../data/userData.json'
@ -52,7 +55,7 @@ function UserListPage() {
const [selectedGroupName, setSelectedGroupName] = useState('')
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
const [filteredUsers, setFilteredUsers] = useState(userData.users)
const [showStatsPanel, setShowStatsPanel] = useState(true)
const [showStatsPanel, setShowStatsPanel] = useState(false)
const [statusFilter, setStatusFilter] = useState(null)
//
@ -477,139 +480,154 @@ function UserListPage() {
title="用户列表"
description="管理系统用户,包括用户信息、权限和授权管理"
showToggle={true}
defaultExpanded={false}
onToggle={(expanded) => setShowStatsPanel(expanded)}
/>
{/* 数据统计面板 */}
{showStatsPanel && (
<div className="stats-panel">
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card
className={`stat-card-small ${statusFilter === null ? '' : 'stat-card-dimmed'}`}
hoverable
onClick={handleTotalClick}
style={{ cursor: 'pointer' }}
>
<Statistic
title="总用户数"
value={userData.users.length}
prefix={<UserOutlined />}
valueStyle={{ color: '#1677ff' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card
className={`stat-card-small ${statusFilter === 'enabled' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
hoverable
onClick={() => handleStatusFilterClick('enabled')}
style={{ cursor: 'pointer' }}
>
<Statistic
title="启用用户"
value={userData.users.filter((u) => u.status === 'enabled').length}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card
className={`stat-card-small ${statusFilter === 'disabled' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
hoverable
onClick={() => handleStatusFilterClick('disabled')}
style={{ cursor: 'pointer' }}
>
<Statistic
title="停用用户"
value={userData.users.filter((u) => u.status === 'disabled').length}
prefix={<CloseCircleOutlined />}
valueStyle={{ color: '#8c8c8c' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card className="stat-card-small stat-card-result">
<Statistic
title="筛选结果"
value={filteredUsers.length}
prefix={<SearchOutlined />}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
</Row>
</div>
)}
{/* 操作栏 - 使用新组件 */}
<ListActionBar
actions={[
{
key: 'add',
label: '新增用户',
icon: <PlusOutlined />,
type: 'primary',
onClick: () => {
setEditMode('add')
setSelectedUser(null)
setShowDetailDrawer(false)
setShowEditDrawer(true)
},
},
{
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="请选择用户分组进行筛选"
{/* 使用 SplitLayout 纵向布局 */}
<SplitLayout
direction="vertical"
mainContent={
<>
{/* 操作栏 - 使用新组件 */}
<ListActionBar
actions={[
{
key: 'add',
label: '新增用户',
icon: <PlusOutlined />,
type: 'primary',
onClick: () => {
setEditMode('add')
setSelectedUser(null)
setShowDetailDrawer(false)
setShowEditDrawer(true)
},
},
{
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="请选择用户分组进行筛选"
/>
),
title: '高级筛选',
visible: showFilterPopover,
onVisibleChange: (visible) => {
setShowFilterPopover(visible)
if (visible) {
setTempSelectedGroup(selectedGroup)
}
},
selectedLabel: selectedGroupName,
isActive: !!selectedGroup,
}}
showRefresh
onRefresh={() => console.log('刷新')}
/>
),
title: '高级筛选',
visible: showFilterPopover,
onVisibleChange: (visible) => {
setShowFilterPopover(visible)
if (visible) {
setTempSelectedGroup(selectedGroup)
}
},
selectedLabel: selectedGroupName,
isActive: !!selectedGroup,
}}
showRefresh
onRefresh={() => console.log('刷新')}
/>
{/* 数据表格 - 使用新组件 */}
<ListTable
columns={columns}
dataSource={filteredUsers}
selectedRowKeys={selectedRowKeys}
onSelectionChange={setSelectedRowKeys}
onRowClick={handleRowClick}
selectedRow={selectedUser}
scroll={{ x: 1400 }}
{/* 数据表格 - 使用新组件 */}
<ListTable
columns={columns}
dataSource={filteredUsers}
selectedRowKeys={selectedRowKeys}
onSelectionChange={setSelectedRowKeys}
onRowClick={handleRowClick}
selectedRow={selectedUser}
scroll={{ x: 1400 }}
/>
</>
}
extendContent={
<ExtendInfoPanel
layout="horizontal"
sections={[
{
key: 'stats',
title: '数据统计',
icon: <DashboardOutlined />,
defaultCollapsed: false,
hideTitleBar: true,
content: (
<div className="stat-cards-grid stat-cards-grid-4">
<StatCard
key="total"
title="总用户数"
value={userData.users.length}
icon={<UserOutlined />}
color="blue"
className={statusFilter === null ? '' : 'stat-card-dimmed'}
onClick={handleTotalClick}
style={{ cursor: 'pointer' }}
/>
<StatCard
key="enabled"
title="启用用户"
value={userData.users.filter((u) => u.status === 'enabled').length}
icon={<CheckCircleOutlined />}
color="green"
className={
statusFilter === 'enabled'
? 'stat-card-active'
: statusFilter !== null
? 'stat-card-dimmed'
: ''
}
onClick={() => handleStatusFilterClick('enabled')}
style={{ cursor: 'pointer' }}
/>
<StatCard
key="disabled"
title="停用用户"
value={userData.users.filter((u) => u.status === 'disabled').length}
icon={<CloseCircleOutlined />}
color="gray"
className={
statusFilter === 'disabled'
? 'stat-card-active'
: statusFilter !== null
? 'stat-card-dimmed'
: ''
}
onClick={() => handleStatusFilterClick('disabled')}
style={{ cursor: 'pointer' }}
/>
<StatCard
key="filtered"
title="筛选结果"
value={filteredUsers.length}
icon={<SearchOutlined />}
color="orange"
/>
</div>
),
},
]}
/>
}
showExtend={showStatsPanel}
extendPosition="top"
/>
{/* 详情抽屉 - 使用新组件 */}

View File

@ -0,0 +1,33 @@
/* 虚拟机镜像页面 */
.vm-image-page {
width: 100%;
}
/* 统计卡片网格 */
.stat-cards-grid {
display: grid;
gap: 12px;
}
/* 不同列数的网格布局 */
.stat-cards-grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.stat-cards-grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.stat-cards-grid-4 {
grid-template-columns: repeat(4, 1fr);
}
/* 响应式 */
@media (max-width: 768px) {
.stat-cards-grid,
.stat-cards-grid-2,
.stat-cards-grid-3,
.stat-cards-grid-4 {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,319 @@
import { useState } from 'react'
import { Button, Tag, Space } from 'antd'
import {
PlusOutlined,
DeleteOutlined,
DatabaseOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
LineChartOutlined,
PieChartOutlined,
DashboardOutlined,
} from '@ant-design/icons'
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
import ListActionBar from '../components/ListActionBar/ListActionBar'
import ListTable from '../components/ListTable/ListTable'
import SplitLayout from '../components/SplitLayout/SplitLayout'
import ExtendInfoPanel from '../components/ExtendInfoPanel/ExtendInfoPanel'
import StatCard from '../components/StatCard/StatCard'
import ChartPanel from '../components/ChartPanel/ChartPanel'
import vmImageData from '../data/vmImageData.json'
import './VirtualMachineImagePage.css'
function VirtualMachineImagePage() {
const [selectedRowKeys, setSelectedRowKeys] = useState([])
const [searchKeyword, setSearchKeyword] = useState('')
const [filteredImages, setFilteredImages] = useState(vmImageData.images)
const [showExtendPanel, setShowExtendPanel] = useState(true)
//
const columns = [
{
title: '序号',
dataIndex: 'id',
key: 'id',
width: 80,
align: 'center',
},
{
title: '镜像名称',
dataIndex: 'name',
key: 'name',
width: 250,
ellipsis: true,
},
{
title: '操作系统',
dataIndex: 'os',
key: 'os',
width: 120,
},
{
title: '版本',
dataIndex: 'version',
key: 'version',
width: 120,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'center',
render: (status) => {
const colorMap = {
running: 'green',
stopped: 'default',
error: 'red',
}
const textMap = {
running: '运行中',
stopped: '已停止',
error: '错误',
}
return <Tag color={colorMap[status]}>{textMap[status]}</Tag>
},
},
{
title: 'CPU使用率',
dataIndex: 'cpuUsage',
key: 'cpuUsage',
width: 110,
align: 'center',
render: (value) => `${value}%`,
},
{
title: '内存使用率',
dataIndex: 'memoryUsage',
key: 'memoryUsage',
width: 110,
align: 'center',
render: (value) => `${value}%`,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
render: (_, record) => (
<Space size="small" onClick={(e) => e.stopPropagation()}>
<Button type="link" size="small">
启动
</Button>
<Button type="link" size="small">
停止
</Button>
<Button type="link" size="small" icon={<DeleteOutlined />} danger>
删除
</Button>
</Space>
),
},
]
//
const handleSearch = (value) => {
setSearchKeyword(value)
if (value) {
const filtered = vmImageData.images.filter(
(image) =>
image.name.toLowerCase().includes(value.toLowerCase()) ||
image.os.toLowerCase().includes(value.toLowerCase())
)
setFilteredImages(filtered)
} else {
setFilteredImages(vmImageData.images)
}
}
//
const handleBatchDelete = () => {
console.log('批量删除', selectedRowKeys)
}
//
const totalCount = vmImageData.images.length
const runningCount = vmImageData.images.filter((img) => img.status === 'running').length
const stoppedCount = vmImageData.images.filter((img) => img.status === 'stopped').length
const errorCount = vmImageData.images.filter((img) => img.status === 'error').length
// CPU使
const cpuTrendData = {
xAxis: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
series: [25, 32, 45, 58, 42, 38],
}
// 使
const memoryTrendData = {
xAxis: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
series: [40, 48, 55, 62, 58, 52],
}
//
const statusDistData = {
series: [
{ value: runningCount, name: '运行中', itemStyle: { color: '#52c41a' } },
{ value: stoppedCount, name: '已停止', itemStyle: { color: '#8c8c8c' } },
{ value: errorCount, name: '错误', itemStyle: { color: '#ff4d4f' } },
],
}
//
const osDistData = {
xAxis: ['Windows', 'Linux', 'Ubuntu', 'CentOS'],
series: [12, 8, 7, 5],
}
return (
<div className="vm-image-page">
<PageTitleBar
title="虚拟机镜像"
description="管理虚拟机镜像,查看性能和使用情况"
showToggle={true}
defaultExpanded={true}
onToggle={(expanded) => setShowExtendPanel(expanded)}
/>
<SplitLayout
direction="horizontal"
mainContent={
<>
<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,
}}
showRefresh
onRefresh={() => console.log('刷新')}
/>
<ListTable
columns={columns}
dataSource={filteredImages}
selectedRowKeys={selectedRowKeys}
onSelectionChange={setSelectedRowKeys}
scroll={{ x: 1400 }}
/>
</>
}
extendContent={
<ExtendInfoPanel
sections={[
{
key: 'overview',
title: '概览',
icon: <DashboardOutlined />,
content: (
<div className="stat-cards-grid stat-cards-grid-2">
<StatCard
key="total"
title="镜像总数"
value={totalCount}
icon={<DatabaseOutlined />}
color="blue"
gridColumn="1 / -1"
/>
<StatCard
key="running"
title="运行中"
value={runningCount}
icon={<CheckCircleOutlined />}
color="green"
trend={{ value: 12, direction: 'up' }}
/>
<StatCard
key="stopped"
title="已停止"
value={stoppedCount}
icon={<CloseCircleOutlined />}
color="gray"
/>
<StatCard
key="error"
title="错误"
value={errorCount}
icon={<CloseCircleOutlined />}
color="red"
trend={{ value: 5, direction: 'down' }}
gridColumn="1 / -1"
/>
</div>
),
},
{
key: 'status',
title: '状态统计',
icon: <PieChartOutlined />,
content: (
<>
<ChartPanel key="status-ring" type="ring" data={statusDistData} height={200} />
<ChartPanel
key="os-bar"
type="bar"
title="操作系统分布"
data={osDistData}
height={200}
/>
</>
),
},
{
key: 'monitor',
title: '性能监控',
icon: <LineChartOutlined />,
defaultCollapsed: false,
content: (
<>
<ChartPanel
key="cpu-trend"
type="line"
title="CPU使用率趋势"
data={cpuTrendData}
height={180}
/>
<ChartPanel
key="memory-trend"
type="line"
title="内存使用率趋势"
data={memoryTrendData}
height={180}
/>
</>
),
},
]}
/>
}
extendSize={360}
showExtend={showExtendPanel}
/>
</div>
)
}
export default VirtualMachineImagePage

View File

@ -66,11 +66,11 @@ body {
.content-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
padding: 16px;
}
.page-header {
margin-bottom: 24px;
margin-bottom: 8px;
}
.card-shadow {

View File

@ -1134,6 +1134,14 @@ eastasianwidth@^0.2.0:
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
echarts@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz"
integrity sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==
dependencies:
tslib "2.3.0"
zrender "6.0.0"
electron-to-chromium@^1.5.238:
version "1.5.244"
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz"
@ -4087,6 +4095,11 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz"
@ -4371,6 +4384,13 @@ yocto-queue@^0.1.0:
resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zrender@6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz"
integrity sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==
dependencies:
tslib "2.3.0"
zwitch@^2.0.0:
version "2.0.4"
resolved "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz"