重定义了几个新组件
parent
aafc2c6bf0
commit
e9691882f0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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` 完整配置。
|
||||
26
README.md
26
README.md
|
|
@ -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 部署支持
|
||||
- 配置完整的部署文档
|
||||
|
||||
## 许可证
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
这就是你想要的方案!🎯
|
||||
|
|
@ -13,6 +13,7 @@ services:
|
|||
- NODE_ENV=production
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./docs:/app/dist/docs
|
||||
networks:
|
||||
- nex-network
|
||||
healthcheck:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
#### 问题 3:Windows 路径问题
|
||||
|
||||
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` - 快速参考
|
||||
|
||||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
|
@ -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 padding,children 内容不需要额外添加 padding
|
||||
4. **操作按钮**:操作按钮数量不宜过多,建议不超过 3 个
|
||||
5. **标签页**:使用 `tabs` 时,第一个标签页默认激活
|
||||
6. **状态清理**:关闭抽屉时建议清空选中状态,避免下次打开时显示旧数据
|
||||
7. **InfoPanel 集成**:配合 InfoPanel 使用时,InfoPanel 会自动处理内部样式,不需要额外的容器包裹
|
||||
|
|
|
|||
|
|
@ -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) - 图表面板组件
|
||||
|
|
@ -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 类名供自定义样式:
|
||||
|
|
|
|||
|
|
@ -367,6 +367,24 @@ function UserListPage() {
|
|||
- `.list-table-container` - 表格容器
|
||||
- `.row-selected` - 选中行的类名
|
||||
|
||||
### 固定高度设计
|
||||
|
||||
ListTable 组件采用固定表格体高度设计,确保页面布局的稳定性:
|
||||
|
||||
- **尺寸**:使用 Ant Design `size="middle"` 属性
|
||||
- **行高**:47px(Ant Design middle 尺寸默认值)
|
||||
- **表格体高度**:470px(固定高度,内部滚动)
|
||||
- **显示行数**:10 行(470px ÷ 47px = 10)
|
||||
- **分页器高度**:56px(Ant Design 默认)
|
||||
- **容器内边距**:16px × 2
|
||||
|
||||
**设计说明**:
|
||||
- 组件使用 Ant Design 的标准 `size="middle"` 属性,不自定义行高
|
||||
- 表格体固定 470px 高度,恰好显示 10 行数据
|
||||
- 超过 10 行的内容通过表格体内部滚动查看
|
||||
- 当数据不足 10 行时,表格体仍保持 470px 高度,确保布局稳定
|
||||
- 分页器上边距 16px,与表格体保持适当间距
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **用户列表** - 显示和管理用户数据
|
||||
|
|
|
|||
|
|
@ -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 内部
|
||||
|
|
|
|||
|
|
@ -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 个核心组件文档
|
||||
|
||||
## 相关文档
|
||||
|
||||
- **主内容区布局** - 详细的布局使用指南
|
||||
- **设计手册** - 设计规范和最佳实践
|
||||
|
|
|
|||
|
|
@ -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) - 图表面板组件
|
||||
|
|
@ -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** - 图表面板(配合使用)
|
||||
|
|
@ -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`
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 "清理完成!"
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@
|
|||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 24px;
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ function PageTitleBar({
|
|||
actions,
|
||||
showToggle = false,
|
||||
onToggle,
|
||||
defaultExpanded = true,
|
||||
defaultExpanded = false,
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded)
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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": "--"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
{
|
||||
"key": "image-system",
|
||||
"label": "系统镜像",
|
||||
"path": "/image/list"
|
||||
"path": "/image/system"
|
||||
},
|
||||
{
|
||||
"key": "image-vm",
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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('')
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
{/* 详情抽屉 - 使用新组件 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
{/* 详情抽屉 - 使用新组件 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
20
yarn.lock
20
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue