重定义了几个新组件
parent
aafc2c6bf0
commit
e9691882f0
|
|
@ -11,6 +11,29 @@
|
||||||
- Nginx serve: 静态文件服务器
|
- Nginx serve: 静态文件服务器
|
||||||
- Node.js 18: 运行环境
|
- 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
|
### 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
|
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
|
- NODE_ENV=production
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
|
- ./docs:/app/dist/docs
|
||||||
networks:
|
networks:
|
||||||
- nex-network
|
- nex-network
|
||||||
healthcheck:
|
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 主要内容 │
|
│ - children 主要内容 │
|
||||||
│ - tabs 标签页(可选) │
|
│ - 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 类名供自定义样式:
|
组件提供以下 CSS 类名供自定义样式:
|
||||||
|
|
@ -460,10 +530,10 @@ function UserListPage() {
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. 抽屉宽度默认 1080px,可根据内容调整,建议取值范围:720-1200px
|
1. **宽度选择**:抽屉宽度默认 1080px,可根据内容调整,建议取值范围:720-1200px
|
||||||
2. 标题栏固定在顶部,不随内容滚动,确保操作按钮始终可见
|
2. **固定头部**:标题栏固定在顶部,不随内容滚动,确保操作按钮始终可见
|
||||||
3. `children` 内容区域会自动应用内边距,`tabs` 内容需要自行控制样式
|
3. **内容 padding**:`detail-drawer-scrollable-content` 已经统一设置了 24px padding,children 内容不需要额外添加 padding
|
||||||
4. 操作按钮数量不宜过多,建议不超过 3 个
|
4. **操作按钮**:操作按钮数量不宜过多,建议不超过 3 个
|
||||||
5. 使用 `tabs` 时,第一个标签页默认激活
|
5. **标签页**:使用 `tabs` 时,第一个标签页默认激活
|
||||||
6. 关闭抽屉时建议清空选中状态,避免下次打开时显示旧数据
|
6. **状态清理**:关闭抽屉时建议清空选中状态,避免下次打开时显示旧数据
|
||||||
7. 配合 InfoPanel 使用时,InfoPanel 会自动处理内边距
|
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 栅格系统:
|
组件使用 Ant Design 的 24 栅格系统:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -276,6 +278,51 @@ const fields = [
|
||||||
- `span=12` - 一行 2 列
|
- `span=12` - 一行 2 列
|
||||||
- `span=24` - 占满整行(适合描述、备注等长文本字段)
|
- `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 类名供自定义样式:
|
组件提供以下 CSS 类名供自定义样式:
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,24 @@ function UserListPage() {
|
||||||
- `.list-table-container` - 表格容器
|
- `.list-table-container` - 表格容器
|
||||||
- `.row-selected` - 选中行的类名
|
- `.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. **用户列表** - 显示和管理用户数据
|
1. **用户列表** - 显示和管理用户数据
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ src/components/PageTitleBar/PageTitleBar.css
|
||||||
| description | string | 否 | - | 页面描述文本,显示在标题下方 |
|
| description | string | 否 | - | 页面描述文本,显示在标题下方 |
|
||||||
| actions | ReactNode | 否 | - | 右侧操作按钮区域内容 |
|
| actions | ReactNode | 否 | - | 右侧操作按钮区域内容 |
|
||||||
| showToggle | boolean | 否 | false | 是否显示展开/收起按钮 |
|
| showToggle | boolean | 否 | false | 是否显示展开/收起按钮 |
|
||||||
| onToggle | function(expanded: boolean) | 否 | - | 展开/收起状态变化时的回调函数 |
|
| onToggle | function(expanded: boolean) | 否 | - | 展开/收起状态变化时的回调函数,接收当前展开状态 |
|
||||||
| defaultExpanded | boolean | 否 | true | 默认展开状态 |
|
| defaultExpanded | boolean | 否 | false | 默认展开状态 |
|
||||||
|
|
||||||
## 使用示例
|
## 使用示例
|
||||||
|
|
||||||
|
|
@ -59,10 +59,12 @@ import { Tag } from 'antd'
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import { useState } from 'react'
|
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'
|
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||||
|
|
||||||
function MyPage() {
|
function MyPage() {
|
||||||
const [showStatsPanel, setShowStatsPanel] = useState(true)
|
const [showStatsPanel, setShowStatsPanel] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -70,13 +72,55 @@ function MyPage() {
|
||||||
title="主机列表"
|
title="主机列表"
|
||||||
description="查看和管理所有接入的主机终端"
|
description="查看和管理所有接入的主机终端"
|
||||||
showToggle={true}
|
showToggle={true}
|
||||||
|
defaultExpanded={false}
|
||||||
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 可展开/收起的内容区域 */}
|
{/* 可展开/收起的统计面板 */}
|
||||||
{showStatsPanel && (
|
{showStatsPanel && (
|
||||||
<div className="stats-panel">
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,7 +150,7 @@ import { PlusOutlined } from '@ant-design/icons'
|
||||||
组件提供以下 CSS 类名供自定义样式:
|
组件提供以下 CSS 类名供自定义样式:
|
||||||
|
|
||||||
- `.page-title-bar` - 组件根容器
|
- `.page-title-bar` - 组件根容器
|
||||||
- `.title-bar-content` - 内容容器
|
- `.title-bar-content` - 标题栏内容容器
|
||||||
- `.title-bar-left` - 左侧内容区域
|
- `.title-bar-left` - 左侧内容区域
|
||||||
- `.title-bar-right` - 右侧内容区域
|
- `.title-bar-right` - 右侧内容区域
|
||||||
- `.title-group` - 标题和徽章组合
|
- `.title-group` - 标题和徽章组合
|
||||||
|
|
@ -120,12 +164,26 @@ import { PlusOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
1. **列表页面** - 显示列表页面的标题和描述
|
1. **列表页面** - 显示列表页面的标题和描述
|
||||||
2. **详情页面** - 显示详情页的标题和状态标签
|
2. **详情页面** - 显示详情页的标题和状态标签
|
||||||
3. **带统计面板的页面** - 配合展开/收起功能控制统计信息的显示
|
3. **带统计面板的页面** - 通过展开/收起按钮控制外部统计面板的显示,保持页面简洁
|
||||||
4. **需要快捷操作的页面** - 通过 actions 参数在标题栏添加常用操作按钮
|
4. **需要快捷操作的页面** - 通过 actions 参数在标题栏添加常用操作按钮
|
||||||
|
|
||||||
|
## 设计理念
|
||||||
|
|
||||||
|
PageTitleBar 采用**分体式设计**,组件本身只负责标题栏的展示和展开/收起状态管理,不包含扩展内容。这种设计具有以下优势:
|
||||||
|
|
||||||
|
1. **职责单一** - 组件专注于标题栏功能,代码更清晰
|
||||||
|
2. **灵活性高** - 扩展内容由父组件管理,可以自由定制样式和布局
|
||||||
|
3. **易于维护** - 标题栏和扩展内容相互独立,修改互不影响
|
||||||
|
4. **复用性强** - 同一个 PageTitleBar 可以控制不同类型的扩展内容
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. `title` 参数为必填项,建议简洁明了
|
1. `title` 参数为必填项,建议简洁明了
|
||||||
2. 当使用 `showToggle` 时,建议同时提供 `onToggle` 回调以响应状态变化
|
2. 当使用 `showToggle` 时:
|
||||||
3. `badge` 参数支持任何 React 节点,常用的如 Ant Design 的 Tag、Badge 组件
|
- 必须提供 `onToggle` 回调函数来响应状态变化
|
||||||
4. `actions` 区域不建议放置过多按钮,以保持界面简洁
|
- 在父组件中管理扩展内容的显示/隐藏状态
|
||||||
|
- 扩展内容应该独立于 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 使用
|
- 配合 ListActionBar 使用
|
||||||
|
|
||||||
4. **[ListTable](./ListTable.md)** - 列表表格组件
|
6. **ListTable** - 列表表格组件
|
||||||
- 统一的表格样式和交互
|
- 统一的表格样式和交互
|
||||||
- 支持行选择、分页、排序
|
- 支持行选择、分页、排序
|
||||||
- 适用于所有列表页面
|
- 适用于所有列表页面
|
||||||
|
|
||||||
### 详情展示组件
|
### 详情展示组件
|
||||||
|
|
||||||
5. **[DetailDrawer](./DetailDrawer.md)** - 详情抽屉组件
|
7. **DetailDrawer** - 详情抽屉组件
|
||||||
- 从右侧滑出的详情面板
|
- 从右侧滑出的详情面板
|
||||||
- 支持标签页和操作按钮
|
- 支持标签页和操作按钮
|
||||||
- 固定头部,内容可滚动
|
- 固定头部,内容可滚动
|
||||||
|
|
||||||
6. **[InfoPanel](./InfoPanel.md)** - 信息展示面板组件
|
8. **InfoPanel** - 信息展示面板组件
|
||||||
- 网格布局展示结构化数据
|
- 网格布局展示结构化数据
|
||||||
- 支持自定义字段渲染
|
- 支持自定义字段渲染
|
||||||
- 配合 DetailDrawer 使用
|
- 配合 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 (结果反馈)
|
操作 → ConfirmDialog (确认) → Toast (结果反馈)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 组件组合示例
|
### 组件依赖关系
|
||||||
|
|
||||||
### 标准列表页面
|
```
|
||||||
|
PageTitleBar (独立使用)
|
||||||
|
|
||||||
|
SplitLayout (布局容器)
|
||||||
|
├─ mainContent
|
||||||
|
│ ├─ ListActionBar
|
||||||
|
│ │ └─ TreeFilterPanel
|
||||||
|
│ └─ ListTable
|
||||||
|
│ └─ DetailDrawer
|
||||||
|
│ └─ InfoPanel
|
||||||
|
└─ extendContent
|
||||||
|
└─ ExtendInfoPanel
|
||||||
|
├─ StatCard
|
||||||
|
└─ ChartPanel
|
||||||
|
|
||||||
|
ConfirmDialog (全局调用)
|
||||||
|
Toast (全局调用)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 典型页面组合
|
||||||
|
|
||||||
|
### 1. 标准列表页面(横向布局)
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<PageTitleBar title="用户列表" description="..." />
|
<PageTitleBar title="虚拟机镜像" description="..." />
|
||||||
|
|
||||||
<ListActionBar
|
<SplitLayout
|
||||||
actions={[...]}
|
direction="horizontal"
|
||||||
search={{...}}
|
mainContent={
|
||||||
filter={{
|
<>
|
||||||
content: <TreeFilterPanel {...} />
|
<ListActionBar
|
||||||
}}
|
actions={[...]}
|
||||||
/>
|
search={{...}}
|
||||||
|
/>
|
||||||
<ListTable
|
<ListTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
onRowClick={showDetail}
|
onRowClick={showDetail}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
extendContent={
|
||||||
|
<ExtendInfoPanel
|
||||||
|
sections={[
|
||||||
|
{
|
||||||
|
key: 'overview',
|
||||||
|
title: '概览',
|
||||||
|
content: <StatCards />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'monitor',
|
||||||
|
title: '性能监控',
|
||||||
|
content: <Charts />
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DetailDrawer visible={showDrawer}>
|
<DetailDrawer visible={showDrawer}>
|
||||||
|
|
@ -104,7 +223,49 @@
|
||||||
</DetailDrawer>
|
</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
|
```jsx
|
||||||
// 1. 点击删除按钮
|
// 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. 查看对应组件的详细文档
|
1. 从左侧菜单选择组件查看详细文档
|
||||||
2. 了解组件的参数配置
|
2. 了解组件的参数配置和使用场景
|
||||||
3. 参考示例代码
|
3. 参考示例代码进行开发
|
||||||
4. 根据实际需求调整参数
|
4. 根据实际需求调整参数
|
||||||
|
|
||||||
### 设计原则
|
### 设计原则
|
||||||
|
|
@ -137,13 +327,29 @@ ConfirmDialog.delete({
|
||||||
- **可复用** - 组件高度封装,易于复用
|
- **可复用** - 组件高度封装,易于复用
|
||||||
- **可配置** - 提供丰富的配置选项
|
- **可配置** - 提供丰富的配置选项
|
||||||
- **易用性** - API 设计简洁直观
|
- **易用性** - API 设计简洁直观
|
||||||
|
- **响应式** - 自适应不同屏幕尺寸
|
||||||
|
|
||||||
### 技术栈
|
### 技术栈
|
||||||
|
|
||||||
- React 18
|
- React 18
|
||||||
- Ant Design 5.x
|
- Ant Design 5.x
|
||||||
|
- ECharts 5.x
|
||||||
- CSS Modules
|
- 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 个核心组件文档
|
- 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": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"antd": "^5.12.0",
|
"antd": "^5.12.0",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|
@ -2003,6 +2004,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.244",
|
"version": "1.5.244",
|
||||||
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
|
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
|
||||||
|
|
@ -7379,6 +7390,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|
@ -7999,6 +8016,15 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"clean": "bash scripts/clean.sh",
|
||||||
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0"
|
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"antd": "^5.12.0",
|
"antd": "^5.12.0",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^10.1.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 HostListPage from './pages/HostListPage'
|
||||||
import UserListPage from './pages/UserListPage'
|
import UserListPage from './pages/UserListPage'
|
||||||
import ImageListPage from './pages/ImageListPage'
|
import ImageListPage from './pages/ImageListPage'
|
||||||
|
import VirtualMachineImagePage from './pages/VirtualMachineImagePage'
|
||||||
import DocsPage from './pages/DocsPage'
|
import DocsPage from './pages/DocsPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -14,7 +15,8 @@ function App() {
|
||||||
<Route path="/overview" element={<OverviewPage />} />
|
<Route path="/overview" element={<OverviewPage />} />
|
||||||
<Route path="/host/list" element={<HostListPage />} />
|
<Route path="/host/list" element={<HostListPage />} />
|
||||||
<Route path="/user/list" element={<UserListPage />} />
|
<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 />} />
|
<Route path="/design" element={<DocsPage />} />
|
||||||
{/* 其他路由将在后续添加 */}
|
{/* 其他路由将在后续添加 */}
|
||||||
</Routes>
|
</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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 24px;
|
padding: 16px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
@ -65,13 +65,13 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签页区域 */
|
/* 标签页区域 */
|
||||||
.detail-drawer-tabs {
|
.detail-drawer-tabs {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
padding-top: 16px;
|
padding: 0;
|
||||||
padding-left: 12px;
|
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,8 +85,7 @@
|
||||||
|
|
||||||
.detail-drawer-tabs :global(.ant-tabs-nav) {
|
.detail-drawer-tabs :global(.ant-tabs-nav) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0 24px;
|
margin: 0 0 16px 0;
|
||||||
margin-bottom: 0;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +114,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-drawer-tab-content {
|
.detail-drawer-tab-content {
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
background: #ffffff;
|
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 {
|
.info-panel {
|
||||||
padding: 6px 8px;
|
padding: 0;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 信息区域容器 */
|
/* 信息区域容器 */
|
||||||
.info-panel > :global(.ant-row) {
|
.info-panel > :global(.ant-row) {
|
||||||
padding: 32px;
|
padding: 24px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
@ -25,17 +25,18 @@
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加底部装饰条 */
|
/* 添加左侧装饰条 */
|
||||||
.info-panel-item::before {
|
.info-panel-item::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 3px;
|
left: 0;
|
||||||
bottom: -1px;
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 3px;
|
height: 0;
|
||||||
background: linear-gradient(90deg, #1677ff 0%, #4096ff 100%);
|
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: width 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel-item:hover {
|
.info-panel-item:hover {
|
||||||
|
|
@ -49,7 +50,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel-item:hover::before {
|
.info-panel-item:hover::before {
|
||||||
width: 60px;
|
width: 3px;
|
||||||
|
height: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel-label {
|
.info-panel-label {
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 4px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-action-bar-left,
|
.list-action-bar-left,
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,7 @@
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
}
|
height: 626px;
|
||||||
|
overflow-y: auto;
|
||||||
/* 表格行样式 */
|
width: 100%;
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ function ListTable({
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
showTotal: (total) => `共 ${total} 条`,
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
},
|
},
|
||||||
scroll = { x: 1200 },
|
scroll = { x: 1200},
|
||||||
onRowClick,
|
onRowClick,
|
||||||
selectedRow,
|
selectedRow,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|
@ -45,6 +45,7 @@ function ListTable({
|
||||||
return (
|
return (
|
||||||
<div className={`list-table-container ${className}`}>
|
<div className={`list-table-container ${className}`}>
|
||||||
<Table
|
<Table
|
||||||
|
size="middle"
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
CustomerServiceOutlined,
|
CustomerServiceOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import headerMenuData from '../../constants/headerMenuData.json'
|
import headerMenuData from '../../data/headerMenuData.json'
|
||||||
import logoFull from '../../assets/logo-full.png'
|
import logoFull from '../../assets/logo-full.png'
|
||||||
import './AppHeader.css'
|
import './AppHeader.css'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import menuData from '../../constants/menuData.json'
|
import menuData from '../../data/menuData.json'
|
||||||
import './AppSider.css'
|
import './AppSider.css'
|
||||||
|
|
||||||
const { Sider } = Layout
|
const { Sider } = Layout
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 24px;
|
padding: 16px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,28 @@
|
||||||
transform: translateY(-1px);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.title-bar-content {
|
.title-bar-content {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ function PageTitleBar({
|
||||||
actions,
|
actions,
|
||||||
showToggle = false,
|
showToggle = false,
|
||||||
onToggle,
|
onToggle,
|
||||||
defaultExpanded = true,
|
defaultExpanded = false,
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(defaultExpanded)
|
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",
|
"key": "components",
|
||||||
"label": "组件文档",
|
"label": "组件文档",
|
||||||
|
|
@ -58,17 +74,26 @@
|
||||||
"key": "toast",
|
"key": "toast",
|
||||||
"label": "Toast",
|
"label": "Toast",
|
||||||
"path": "/docs/components/Toast.md"
|
"path": "/docs/components/Toast.md"
|
||||||
}
|
},
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "pages",
|
|
||||||
"label": "页面文档",
|
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"key": "main-layout",
|
"key": "split-layout",
|
||||||
"label": "主布局",
|
"label": "SplitLayout",
|
||||||
"path": "/docs/pages/main-layout.md"
|
"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",
|
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-2503-HWE-Release-2503-X86_64.iso",
|
||||||
"btPath": "--",
|
"btPath": "--",
|
||||||
"description": "--"
|
"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",
|
"key": "image-system",
|
||||||
"label": "系统镜像",
|
"label": "系统镜像",
|
||||||
"path": "/image/list"
|
"path": "/image/system"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "image-vm",
|
"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 rehypeRaw from 'rehype-raw'
|
||||||
import rehypeHighlight from 'rehype-highlight'
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
import 'highlight.js/styles/github.css'
|
import 'highlight.js/styles/github.css'
|
||||||
|
import docsMenuData from '../data/docsMenuData.json'
|
||||||
import './DocsPage.css'
|
import './DocsPage.css'
|
||||||
|
|
||||||
const { Sider, Content } = Layout
|
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() {
|
function DocsPage() {
|
||||||
const [selectedKey, setSelectedKey] = useState('design-cookbook')
|
const [selectedKey, setSelectedKey] = useState('design-cookbook')
|
||||||
const [markdownContent, setMarkdownContent] = useState('')
|
const [markdownContent, setMarkdownContent] = useState('')
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,25 @@
|
||||||
width: 100%;
|
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 特有 */
|
/* 统计面板 - HostListPage 特有 */
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
@ -179,14 +198,20 @@
|
||||||
|
|
||||||
/* 响应式 */
|
/* 响应式 */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.stats-panel :global(.ant-row) {
|
.stat-cards-grid-4 {
|
||||||
display: grid;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards-grid-3 {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@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;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import {
|
||||||
Card,
|
Card,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Statistic,
|
|
||||||
TreeSelect,
|
TreeSelect,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,6 +29,7 @@ import {
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||||||
|
|
@ -37,6 +37,9 @@ import TreeFilterPanel from '../components/TreeFilterPanel/TreeFilterPanel'
|
||||||
import ListTable from '../components/ListTable/ListTable'
|
import ListTable from '../components/ListTable/ListTable'
|
||||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
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 ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||||
import Toast from '../components/Toast/Toast'
|
import Toast from '../components/Toast/Toast'
|
||||||
import hostData from '../data/hostData.json'
|
import hostData from '../data/hostData.json'
|
||||||
|
|
@ -54,7 +57,7 @@ function HostListPage() {
|
||||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||||
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
|
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
|
||||||
const [filteredHosts, setFilteredHosts] = useState(hostData.hosts)
|
const [filteredHosts, setFilteredHosts] = useState(hostData.hosts)
|
||||||
const [showStatsPanel, setShowStatsPanel] = useState(true)
|
const [showStatsPanel, setShowStatsPanel] = useState(false)
|
||||||
const [statusFilter, setStatusFilter] = useState(null)
|
const [statusFilter, setStatusFilter] = useState(null)
|
||||||
|
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
|
|
@ -613,153 +616,168 @@ function HostListPage() {
|
||||||
title="主机列表"
|
title="主机列表"
|
||||||
description="查看和管理所有接入的主机终端,包括服务器和办公设备"
|
description="查看和管理所有接入的主机终端,包括服务器和办公设备"
|
||||||
showToggle={true}
|
showToggle={true}
|
||||||
|
defaultExpanded={false}
|
||||||
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 数据统计面板 */}
|
{/* 使用 SplitLayout 纵向布局 */}
|
||||||
{showStatsPanel && (
|
<SplitLayout
|
||||||
<div className="stats-panel">
|
direction="vertical"
|
||||||
<Row gutter={[16, 16]}>
|
mainContent={
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<>
|
||||||
<Card
|
{/* 操作栏 - 使用新组件 */}
|
||||||
className={`stat-card-small ${statusFilter === null ? '' : 'stat-card-dimmed'}`}
|
<ListActionBar
|
||||||
hoverable
|
actions={[
|
||||||
onClick={handleTotalClick}
|
{
|
||||||
style={{ cursor: 'pointer' }}
|
key: 'add',
|
||||||
>
|
label: '新增主机',
|
||||||
<Statistic
|
icon: <PlusOutlined />,
|
||||||
title="总主机数"
|
type: 'primary',
|
||||||
value={hostData.hosts.length}
|
onClick: () => {
|
||||||
prefix={<DesktopOutlined />}
|
setEditMode('add')
|
||||||
valueStyle={{ color: '#1677ff' }}
|
setSelectedHost(null)
|
||||||
/>
|
setShowDetailDrawer(false)
|
||||||
</Card>
|
setShowEditDrawer(true)
|
||||||
</Col>
|
},
|
||||||
<Col xs={24} sm={12} lg={6}>
|
},
|
||||||
<Card
|
{
|
||||||
className={`stat-card-small ${statusFilter === 'online' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
|
key: 'batchPowerOn',
|
||||||
hoverable
|
label: '批量开机',
|
||||||
onClick={() => handleStatusFilterClick('online')}
|
icon: <PoweroffOutlined />,
|
||||||
style={{ cursor: 'pointer' }}
|
disabled: selectedRowKeys.length === 0,
|
||||||
>
|
onClick: () => console.log('批量开机'),
|
||||||
<Statistic
|
},
|
||||||
title="在线主机"
|
{
|
||||||
value={hostData.hosts.filter((h) => h.status === 'online').length}
|
key: 'batchPowerOff',
|
||||||
prefix={<CheckCircleOutlined />}
|
label: '批量关机',
|
||||||
valueStyle={{ color: '#52c41a' }}
|
icon: <PoweroffOutlined />,
|
||||||
/>
|
disabled: selectedRowKeys.length === 0,
|
||||||
</Card>
|
onClick: () => console.log('批量关机'),
|
||||||
</Col>
|
},
|
||||||
<Col xs={24} sm={12} lg={6}>
|
{
|
||||||
<Card
|
key: 'batchDelete',
|
||||||
className={`stat-card-small ${statusFilter === 'offline' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
|
label: '批量删除',
|
||||||
hoverable
|
icon: <DeleteOutlined />,
|
||||||
onClick={() => handleStatusFilterClick('offline')}
|
danger: true,
|
||||||
style={{ cursor: 'pointer' }}
|
disabled: selectedRowKeys.length === 0,
|
||||||
>
|
onClick: handleBatchDelete,
|
||||||
<Statistic
|
},
|
||||||
title="离线主机"
|
]}
|
||||||
value={hostData.hosts.filter((h) => h.status === 'offline').length}
|
search={{
|
||||||
prefix={<CloseCircleOutlined />}
|
placeholder: '搜索主机名、IP或MAC地址',
|
||||||
valueStyle={{ color: '#8c8c8c' }}
|
value: searchKeyword,
|
||||||
/>
|
onSearch: handleSearch,
|
||||||
</Card>
|
onChange: handleSearch,
|
||||||
</Col>
|
}}
|
||||||
<Col xs={24} sm={12} lg={6}>
|
filter={{
|
||||||
<Card className="stat-card-small stat-card-result">
|
content: (
|
||||||
<Statistic
|
<TreeFilterPanel
|
||||||
title="筛选结果"
|
treeData={treeData}
|
||||||
value={filteredHosts.length}
|
selectedKey={selectedGroup}
|
||||||
prefix={<SearchOutlined />}
|
tempSelectedKey={tempSelectedGroup}
|
||||||
valueStyle={{ color: '#faad14' }}
|
treeTitle="终端分组"
|
||||||
/>
|
onSelect={setTempSelectedGroup}
|
||||||
</Card>
|
onConfirm={handleConfirmFilter}
|
||||||
</Col>
|
onClear={handleClearFilter}
|
||||||
</Row>
|
placeholder="请选择终端分组进行筛选"
|
||||||
</div>
|
/>
|
||||||
)}
|
),
|
||||||
|
title: '高级筛选',
|
||||||
{/* 操作栏 - 使用新组件 */}
|
visible: showFilterPopover,
|
||||||
<ListActionBar
|
onVisibleChange: (visible) => {
|
||||||
actions={[
|
setShowFilterPopover(visible)
|
||||||
{
|
if (visible) {
|
||||||
key: 'add',
|
setTempSelectedGroup(selectedGroup)
|
||||||
label: '新增主机',
|
}
|
||||||
icon: <PlusOutlined />,
|
},
|
||||||
type: 'primary',
|
selectedLabel: selectedGroupName,
|
||||||
onClick: () => {
|
isActive: !!selectedGroup,
|
||||||
setEditMode('add')
|
}}
|
||||||
setSelectedHost(null)
|
showRefresh
|
||||||
setShowDetailDrawer(false)
|
onRefresh={() => console.log('刷新')}
|
||||||
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('刷新')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 数据表格 - 使用新组件 */}
|
{/* 数据表格 - 使用新组件 */}
|
||||||
<ListTable
|
<ListTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={filteredHosts}
|
dataSource={filteredHosts}
|
||||||
selectedRowKeys={selectedRowKeys}
|
selectedRowKeys={selectedRowKeys}
|
||||||
onSelectionChange={setSelectedRowKeys}
|
onSelectionChange={setSelectedRowKeys}
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
selectedRow={selectedHost}
|
selectedRow={selectedHost}
|
||||||
scroll={{ x: 1600 }}
|
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%;
|
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 特有 */
|
/* 统计面板 - UserListPage 特有 */
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
@ -143,14 +162,20 @@
|
||||||
|
|
||||||
/* 响应式 */
|
/* 响应式 */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.stats-panel :global(.ant-row) {
|
.stat-cards-grid-4 {
|
||||||
display: grid;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards-grid-3 {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@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;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,13 @@ import {
|
||||||
Select,
|
Select,
|
||||||
Input,
|
Input,
|
||||||
Divider,
|
Divider,
|
||||||
Card,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Statistic,
|
|
||||||
Switch,
|
Switch,
|
||||||
Badge,
|
Badge,
|
||||||
Drawer,
|
Drawer,
|
||||||
TreeSelect,
|
TreeSelect,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
|
|
@ -28,6 +27,7 @@ import {
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||||||
|
|
@ -35,6 +35,9 @@ import TreeFilterPanel from '../components/TreeFilterPanel/TreeFilterPanel'
|
||||||
import ListTable from '../components/ListTable/ListTable'
|
import ListTable from '../components/ListTable/ListTable'
|
||||||
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
import DetailDrawer from '../components/DetailDrawer/DetailDrawer'
|
||||||
import InfoPanel from '../components/InfoPanel/InfoPanel'
|
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 ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||||
import Toast from '../components/Toast/Toast'
|
import Toast from '../components/Toast/Toast'
|
||||||
import userData from '../data/userData.json'
|
import userData from '../data/userData.json'
|
||||||
|
|
@ -52,7 +55,7 @@ function UserListPage() {
|
||||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||||
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
|
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
|
||||||
const [filteredUsers, setFilteredUsers] = useState(userData.users)
|
const [filteredUsers, setFilteredUsers] = useState(userData.users)
|
||||||
const [showStatsPanel, setShowStatsPanel] = useState(true)
|
const [showStatsPanel, setShowStatsPanel] = useState(false)
|
||||||
const [statusFilter, setStatusFilter] = useState(null)
|
const [statusFilter, setStatusFilter] = useState(null)
|
||||||
|
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
|
|
@ -477,139 +480,154 @@ function UserListPage() {
|
||||||
title="用户列表"
|
title="用户列表"
|
||||||
description="管理系统用户,包括用户信息、权限和授权管理"
|
description="管理系统用户,包括用户信息、权限和授权管理"
|
||||||
showToggle={true}
|
showToggle={true}
|
||||||
|
defaultExpanded={false}
|
||||||
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 数据统计面板 */}
|
{/* 使用 SplitLayout 纵向布局 */}
|
||||||
{showStatsPanel && (
|
<SplitLayout
|
||||||
<div className="stats-panel">
|
direction="vertical"
|
||||||
<Row gutter={[16, 16]}>
|
mainContent={
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<>
|
||||||
<Card
|
{/* 操作栏 - 使用新组件 */}
|
||||||
className={`stat-card-small ${statusFilter === null ? '' : 'stat-card-dimmed'}`}
|
<ListActionBar
|
||||||
hoverable
|
actions={[
|
||||||
onClick={handleTotalClick}
|
{
|
||||||
style={{ cursor: 'pointer' }}
|
key: 'add',
|
||||||
>
|
label: '新增用户',
|
||||||
<Statistic
|
icon: <PlusOutlined />,
|
||||||
title="总用户数"
|
type: 'primary',
|
||||||
value={userData.users.length}
|
onClick: () => {
|
||||||
prefix={<UserOutlined />}
|
setEditMode('add')
|
||||||
valueStyle={{ color: '#1677ff' }}
|
setSelectedUser(null)
|
||||||
/>
|
setShowDetailDrawer(false)
|
||||||
</Card>
|
setShowEditDrawer(true)
|
||||||
</Col>
|
},
|
||||||
<Col xs={24} sm={12} lg={6}>
|
},
|
||||||
<Card
|
{
|
||||||
className={`stat-card-small ${statusFilter === 'enabled' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
|
key: 'batchDelete',
|
||||||
hoverable
|
label: '批量删除',
|
||||||
onClick={() => handleStatusFilterClick('enabled')}
|
icon: <DeleteOutlined />,
|
||||||
style={{ cursor: 'pointer' }}
|
danger: true,
|
||||||
>
|
disabled: selectedRowKeys.length === 0,
|
||||||
<Statistic
|
onClick: handleBatchDelete,
|
||||||
title="启用用户"
|
},
|
||||||
value={userData.users.filter((u) => u.status === 'enabled').length}
|
]}
|
||||||
prefix={<CheckCircleOutlined />}
|
search={{
|
||||||
valueStyle={{ color: '#52c41a' }}
|
placeholder: '搜索用户名或姓名',
|
||||||
/>
|
value: searchKeyword,
|
||||||
</Card>
|
onSearch: handleSearch,
|
||||||
</Col>
|
onChange: handleSearch,
|
||||||
<Col xs={24} sm={12} lg={6}>
|
}}
|
||||||
<Card
|
filter={{
|
||||||
className={`stat-card-small ${statusFilter === 'disabled' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
|
content: (
|
||||||
hoverable
|
<TreeFilterPanel
|
||||||
onClick={() => handleStatusFilterClick('disabled')}
|
treeData={treeData}
|
||||||
style={{ cursor: 'pointer' }}
|
selectedKey={selectedGroup}
|
||||||
>
|
tempSelectedKey={tempSelectedGroup}
|
||||||
<Statistic
|
treeTitle="用户分组"
|
||||||
title="停用用户"
|
onSelect={setTempSelectedGroup}
|
||||||
value={userData.users.filter((u) => u.status === 'disabled').length}
|
onConfirm={handleConfirmFilter}
|
||||||
prefix={<CloseCircleOutlined />}
|
onClear={handleClearFilter}
|
||||||
valueStyle={{ color: '#8c8c8c' }}
|
placeholder="请选择用户分组进行筛选"
|
||||||
/>
|
/>
|
||||||
</Card>
|
),
|
||||||
</Col>
|
title: '高级筛选',
|
||||||
<Col xs={24} sm={12} lg={6}>
|
visible: showFilterPopover,
|
||||||
<Card className="stat-card-small stat-card-result">
|
onVisibleChange: (visible) => {
|
||||||
<Statistic
|
setShowFilterPopover(visible)
|
||||||
title="筛选结果"
|
if (visible) {
|
||||||
value={filteredUsers.length}
|
setTempSelectedGroup(selectedGroup)
|
||||||
prefix={<SearchOutlined />}
|
}
|
||||||
valueStyle={{ color: '#faad14' }}
|
},
|
||||||
/>
|
selectedLabel: selectedGroupName,
|
||||||
</Card>
|
isActive: !!selectedGroup,
|
||||||
</Col>
|
}}
|
||||||
</Row>
|
showRefresh
|
||||||
</div>
|
onRefresh={() => console.log('刷新')}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 操作栏 - 使用新组件 */}
|
|
||||||
<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('刷新')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 数据表格 - 使用新组件 */}
|
{/* 数据表格 - 使用新组件 */}
|
||||||
<ListTable
|
<ListTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={filteredUsers}
|
dataSource={filteredUsers}
|
||||||
selectedRowKeys={selectedRowKeys}
|
selectedRowKeys={selectedRowKeys}
|
||||||
onSelectionChange={setSelectedRowKeys}
|
onSelectionChange={setSelectedRowKeys}
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
selectedRow={selectedUser}
|
selectedRow={selectedUser}
|
||||||
scroll={{ x: 1400 }}
|
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 {
|
.content-container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 24px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-shadow {
|
.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"
|
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
||||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
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:
|
electron-to-chromium@^1.5.238:
|
||||||
version "1.5.244"
|
version "1.5.244"
|
||||||
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz"
|
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"
|
resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
|
||||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
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:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz"
|
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"
|
resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
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:
|
zwitch@^2.0.0:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz"
|
resolved "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue