release: bump version to 0.27.5
This commit is contained in:
@@ -15,6 +15,23 @@ This project follows the repository versioning rule:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.27.5] — 2026-04-14
|
||||||
|
|
||||||
|
### 🔧 Improvements
|
||||||
|
- 统一控制台多页面滚动体验:BGP、alerts、采集数据、用户管理、任务、设置、Playground 等区域接入自定义滚动条与表格滚动容器
|
||||||
|
- 优化 BGP 与 alerts 页响应式布局:顶部概览卡在窄宽度下优先重排,必要时才启用横向滚动,避免卡片裁切和全局滚动条接管
|
||||||
|
- 调整 `situational alerts` 布局策略:统计卡按宽度在单行、两列和横滚之间切换,下方详情卡保持单行高度优先
|
||||||
|
- 实时采集进度优化:一键采集完成后在未刷新页面时保留 100% 完成态,不再错误归零
|
||||||
|
- 补充 UE5 MVP 融合方案文档,完善后续集成规划沉淀
|
||||||
|
|
||||||
|
### 🐛 Fixes
|
||||||
|
- 修复 BGP summary 与 alerts 顶部卡片在无真实溢出时误出现横向滚动的问题
|
||||||
|
- 修复 alerts 页面缩窄后外层全局竖向滚动被接管的问题,恢复“一屏内、内部滚动”的布局逻辑
|
||||||
|
- 修复 `situational alerts` 在两排布局下详情卡竖向溢出的问题,改为更稳定的分区响应式排版
|
||||||
|
- 修复自定义滚动条交互反馈,悬停、聚焦、拖拽时颜色加深但不再显示多余外圈
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.27.3] — 2026-04-14
|
## [0.27.3] — 2026-04-14
|
||||||
|
|
||||||
### 🔧 Improvements
|
### 🔧 Improvements
|
||||||
|
|||||||
981
docs/ue5_mvp_fused_plan.md
Normal file
981
docs/ue5_mvp_fused_plan.md
Normal file
@@ -0,0 +1,981 @@
|
|||||||
|
# 智能星球 UE5 客户端一期实施方案(融合版)
|
||||||
|
|
||||||
|
> 版本:v2.0
|
||||||
|
> 日期:2026-04-14
|
||||||
|
> 目标:把现有 Web Earth 项目,平滑推进到 **UE5 可用 MVP 客户端**
|
||||||
|
> 适用对象:**UE 零基础新手**
|
||||||
|
> 输出结果:一份 **能直接照着做** 的实施手册
|
||||||
|
> 策略:**保留原 MVP 方案里适合入门的部分,吸收更稳的工程做法,降低你第一次做 UE 时踩坑概率**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、这份融合版方案解决什么问题
|
||||||
|
|
||||||
|
你原来的 MVP 方案是靠谱的,优点很明显:
|
||||||
|
|
||||||
|
- 范围克制
|
||||||
|
- 适合新手入门
|
||||||
|
- 目标明确
|
||||||
|
- 能较快做出“看得见、点得到”的成果
|
||||||
|
|
||||||
|
但它也有几个风险:
|
||||||
|
|
||||||
|
- 默认 `localhost` 一定通,这在 WSL2 + Windows + Docker 环境里不一定成立
|
||||||
|
- 默认 UE 蓝图里直接做 HTTP + JSON 解析会很顺,这一步其实很容易卡
|
||||||
|
- 默认“一上来就接真实后端”,新手会同时踩 UE、Cesium、网络、JSON、蓝图五个坑
|
||||||
|
- 时间估计略乐观
|
||||||
|
|
||||||
|
所以这份融合版方案的核心思路是:
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
**先做“本地数据可交互地球”,再做“真实后端对接”。**
|
||||||
|
|
||||||
|
也就是把一期再拆成两个更稳的里程碑:
|
||||||
|
|
||||||
|
### 里程碑 A:本地演示版
|
||||||
|
先不接后端,只做:
|
||||||
|
|
||||||
|
- UE5 项目能打开
|
||||||
|
- Cesium 地球能显示
|
||||||
|
- 本地 JSON 里的点能正确落到地球
|
||||||
|
- 点击点能弹信息卡
|
||||||
|
- HUD 能正常显示假状态
|
||||||
|
|
||||||
|
### 里程碑 B:后端接入版
|
||||||
|
在 A 的基础上再做:
|
||||||
|
|
||||||
|
- HTTP 拉取真实后端数据
|
||||||
|
- 显示真实 TOP500 数据
|
||||||
|
- 显示后端在线状态
|
||||||
|
- 为后续扩展海缆/BGP/卫星打基础
|
||||||
|
|
||||||
|
这样做的好处是:
|
||||||
|
|
||||||
|
- 把问题拆开
|
||||||
|
- 更容易调试
|
||||||
|
- 更适合 UE 新手
|
||||||
|
- 不会因为后端联调没通就把整个 UE 开发节奏打断
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 二、一期目标:做什么,不做什么
|
||||||
|
|
||||||
|
## 这次一期一定要做的
|
||||||
|
|
||||||
|
做一个 **可用的 UE5 客户端 MVP**,达到以下 6 项:
|
||||||
|
|
||||||
|
1. 能打开 UE 项目并看到 3D 地球
|
||||||
|
2. 能在地球上显示超算数据点
|
||||||
|
3. 能点击数据点弹出信息卡
|
||||||
|
4. 能显示一个基础 HUD
|
||||||
|
5. 能通过 HTTP 接入后端数据
|
||||||
|
6. 能打包成 Windows 可执行程序
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 这次一期先不做的
|
||||||
|
|
||||||
|
这些全部放到后续阶段:
|
||||||
|
|
||||||
|
- 海缆路径渲染
|
||||||
|
- 卫星轨迹与卫星图层
|
||||||
|
- BGP 图层
|
||||||
|
- WebSocket 实时更新
|
||||||
|
- 粒子特效大升级
|
||||||
|
- 自动巡航
|
||||||
|
- 多屏/3D 偏振/大屏联动
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
|
||||||
|
**一期不是“把 Web Earth 全搬到 UE”,而是“证明 UE 客户端链路能跑通”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 三、UE 专有名词字典(零基础版)
|
||||||
|
|
||||||
|
这部分你最好先读一遍。后面所有步骤都围绕这些词。
|
||||||
|
|
||||||
|
## 1. Actor
|
||||||
|
**Actor = 场景里的一个对象**
|
||||||
|
|
||||||
|
你可以把它理解成:
|
||||||
|
|
||||||
|
- 一个地球控制器
|
||||||
|
- 一个超算点
|
||||||
|
- 一台相机
|
||||||
|
- 一条海缆
|
||||||
|
|
||||||
|
这些在 UE 里都可以是 Actor。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Component
|
||||||
|
**Component = 挂在 Actor 身上的功能零件**
|
||||||
|
|
||||||
|
比如一个超算点 Actor,可能有:
|
||||||
|
|
||||||
|
- 一个球形外观
|
||||||
|
- 一个碰撞盒
|
||||||
|
- 一个标签
|
||||||
|
- 一个发光效果
|
||||||
|
|
||||||
|
这些零件就是 Component。
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
|
||||||
|
**Actor 是整台机器,Component 是机器上的零件。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Blueprint(蓝图)
|
||||||
|
**Blueprint = UE 的可视化编程系统**
|
||||||
|
|
||||||
|
你不用先写代码,而是把很多“逻辑节点”拖出来,用线连接起来。
|
||||||
|
|
||||||
|
你可以把它理解成:
|
||||||
|
|
||||||
|
- 前端里的函数 + 事件监听
|
||||||
|
- 只不过不是写文本代码,而是连线
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Level / Map(关卡)
|
||||||
|
**Level = 一个场景文件**
|
||||||
|
|
||||||
|
你可以把它理解成 Three.js 的一个 Scene。
|
||||||
|
|
||||||
|
本期只需要一个主场景:
|
||||||
|
|
||||||
|
- `Main`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Widget / UMG
|
||||||
|
**Widget = UI 组件**
|
||||||
|
**UMG = UE 的 UI 编辑系统**
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
- 信息卡
|
||||||
|
- 状态栏
|
||||||
|
- 右上角连接状态
|
||||||
|
- 图例
|
||||||
|
- HUD 面板
|
||||||
|
|
||||||
|
这些都用 Widget 做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Material(材质)
|
||||||
|
**Material = 决定物体外观的系统**
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
- 球体是什么颜色
|
||||||
|
- 是否发光
|
||||||
|
- 是否透明
|
||||||
|
- 是否随性能大小变亮
|
||||||
|
|
||||||
|
这些都由材质控制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Static Mesh
|
||||||
|
**Static Mesh = 不会变形的 3D 模型**
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
- 球
|
||||||
|
- 立方体
|
||||||
|
- 平面
|
||||||
|
- 某个固定模型
|
||||||
|
|
||||||
|
超算点一期里可以先直接用球体 Static Mesh。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Pawn
|
||||||
|
**Pawn = 玩家控制的对象**
|
||||||
|
|
||||||
|
一期里你可以把它理解成:
|
||||||
|
|
||||||
|
- 带相机的飞行控制器
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. PlayerController
|
||||||
|
**PlayerController = 处理输入的对象**
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
- 鼠标点击
|
||||||
|
- 拖拽
|
||||||
|
- 滚轮缩放
|
||||||
|
|
||||||
|
这些都由 PlayerController 或其相关逻辑来处理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. GameMode
|
||||||
|
**GameMode = 游戏/场景的主规则配置入口**
|
||||||
|
|
||||||
|
它决定:
|
||||||
|
|
||||||
|
- 默认用哪个 Pawn
|
||||||
|
- 默认用哪个 PlayerController
|
||||||
|
|
||||||
|
你可以把它理解成“主入口配置”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Viewport
|
||||||
|
**Viewport = 你看 3D 场景的窗口**
|
||||||
|
|
||||||
|
就是 UE 编辑器中间那块 3D 视图。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Outliner
|
||||||
|
**Outliner = 当前场景对象列表**
|
||||||
|
|
||||||
|
你可以把它理解成:
|
||||||
|
|
||||||
|
- Scene 树
|
||||||
|
- DOM 树
|
||||||
|
- 资源树
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Details Panel
|
||||||
|
**Details Panel = 选中对象后的属性面板**
|
||||||
|
|
||||||
|
相当于“右侧属性编辑器”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Cesium for Unreal
|
||||||
|
**Cesium for Unreal = UE 里的地球插件**
|
||||||
|
|
||||||
|
它负责:
|
||||||
|
|
||||||
|
- 真实地球
|
||||||
|
- 卫星影像
|
||||||
|
- 地形
|
||||||
|
- 经纬度坐标和 UE 世界坐标的转换
|
||||||
|
|
||||||
|
如果没有它,你得自己处理地球和坐标系统,会非常难。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Struct(结构体)
|
||||||
|
**Struct = 数据结构定义**
|
||||||
|
|
||||||
|
你可以把它理解成 TypeScript 里的 `interface`。
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ComputePoint {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
performance: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 UE 里这类东西叫 Struct。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Event Dispatcher
|
||||||
|
**Event Dispatcher = 事件分发器**
|
||||||
|
|
||||||
|
你可以把它理解成:
|
||||||
|
|
||||||
|
- EventEmitter
|
||||||
|
- 发布订阅
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
“数据加载完毕”这个事件,就可以分发给其他蓝图。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Spline
|
||||||
|
**Spline = 一条平滑曲线**
|
||||||
|
|
||||||
|
后面做海缆、轨迹时非常有用。
|
||||||
|
一期可以先知道这个词,不一定马上用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. Niagara
|
||||||
|
**Niagara = UE 粒子特效系统**
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
- 流光
|
||||||
|
- 光晕
|
||||||
|
- 拖尾
|
||||||
|
- 火花
|
||||||
|
|
||||||
|
一期先不重点碰它。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 四、你的真实开发策略:两阶段起步
|
||||||
|
|
||||||
|
这是这份融合版和原方案最大的区别。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 阶段 A:本地演示版(先脱离后端)
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
先把下面这些完全打通:
|
||||||
|
|
||||||
|
- UE 项目启动正常
|
||||||
|
- Cesium 地球正常
|
||||||
|
- 相机可操作
|
||||||
|
- 本地 JSON 文件能生成地球标记点
|
||||||
|
- 点击点能弹信息卡
|
||||||
|
- HUD 能显示假数据
|
||||||
|
|
||||||
|
### 为什么一定要先做这个
|
||||||
|
因为如果你一上来就接真实后端,你会同时碰到:
|
||||||
|
|
||||||
|
- WSL2 到 Windows 网络
|
||||||
|
- Docker 端口映射
|
||||||
|
- UE HTTP 请求
|
||||||
|
- 蓝图 JSON 解析
|
||||||
|
- Cesium 坐标转换
|
||||||
|
- 标记点生成
|
||||||
|
|
||||||
|
新手很容易直接乱掉。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 阶段 B:后端接入版(再联调)
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
在 A 的基础上,加上:
|
||||||
|
|
||||||
|
- HTTP 拉真实后端数据
|
||||||
|
- 显示真实 TOP500 点
|
||||||
|
- 右上角显示后端在线状态
|
||||||
|
- 为后续做更多图层留下数据接入层
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 五、环境准备
|
||||||
|
|
||||||
|
## 1. 你要安装的软件
|
||||||
|
|
||||||
|
### Epic Games Launcher
|
||||||
|
用来下载和启动 UE。
|
||||||
|
|
||||||
|
### Unreal Engine 5.4
|
||||||
|
建议直接用 5.4 稳定版。
|
||||||
|
|
||||||
|
### Visual Studio 2022
|
||||||
|
虽然一期主要用 Blueprint,但 UE 的很多项目依赖 VS 环境。
|
||||||
|
|
||||||
|
安装组件:
|
||||||
|
- Desktop development with C++
|
||||||
|
- Game development with C++
|
||||||
|
|
||||||
|
### Git
|
||||||
|
用来管理文档和后续工程。
|
||||||
|
|
||||||
|
### Cesium for Unreal
|
||||||
|
用来做地球。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 你的环境约束
|
||||||
|
|
||||||
|
你现在是:
|
||||||
|
|
||||||
|
- 后端可能跑在 WSL2 / Docker
|
||||||
|
- UE 必须跑在 Windows
|
||||||
|
|
||||||
|
所以你的真实运行方式通常会是:
|
||||||
|
|
||||||
|
- **Windows** 运行 UE5
|
||||||
|
- **WSL2** 运行后端
|
||||||
|
- 两者通过 HTTP 通信
|
||||||
|
|
||||||
|
这里最关键的一条是:
|
||||||
|
|
||||||
|
**不要默认 `localhost` 一定能通,必须先在 Windows 浏览器里验证。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 六、推荐的项目结构
|
||||||
|
|
||||||
|
## UE 项目目录内的 Content 结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
Content/
|
||||||
|
Blueprints/
|
||||||
|
Data/
|
||||||
|
Widgets/
|
||||||
|
Materials/
|
||||||
|
Levels/
|
||||||
|
FX/
|
||||||
|
Textures/
|
||||||
|
```
|
||||||
|
|
||||||
|
建议说明:
|
||||||
|
|
||||||
|
- `Blueprints/` 放逻辑蓝图
|
||||||
|
- `Data/` 放本地 JSON、DataTable、Struct
|
||||||
|
- `Widgets/` 放 UI
|
||||||
|
- `Materials/` 放材质
|
||||||
|
- `Levels/` 放场景
|
||||||
|
- `FX/` 放特效
|
||||||
|
- `Textures/` 放贴图
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 七、一期最小蓝图清单
|
||||||
|
|
||||||
|
一期只需要这几个核心蓝图。
|
||||||
|
|
||||||
|
## 1. `BP_GlobeCamera`
|
||||||
|
作用:相机控制器
|
||||||
|
|
||||||
|
负责:
|
||||||
|
- 鼠标拖拽旋转
|
||||||
|
- 滚轮缩放
|
||||||
|
- 初始视角控制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `BP_PlanetGameMode`
|
||||||
|
作用:指定默认的 Pawn 等
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `BP_DataLoader`
|
||||||
|
作用:负责读数据
|
||||||
|
|
||||||
|
一期建议支持两种来源:
|
||||||
|
|
||||||
|
- 本地 JSON
|
||||||
|
- HTTP 接口
|
||||||
|
|
||||||
|
这样调试更稳。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `BP_ComputePoint`
|
||||||
|
作用:一个超算点的显示对象
|
||||||
|
|
||||||
|
负责:
|
||||||
|
- 接收一条数据
|
||||||
|
- 放到正确经纬度位置
|
||||||
|
- 显示外观
|
||||||
|
- 处理点击
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. `WBP_InfoCard`
|
||||||
|
作用:点开后显示详情
|
||||||
|
|
||||||
|
显示:
|
||||||
|
- 名称
|
||||||
|
- 国家
|
||||||
|
- 算力
|
||||||
|
- 可选显示更多字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. `WBP_StatusBar`
|
||||||
|
作用:右上角状态栏
|
||||||
|
|
||||||
|
显示:
|
||||||
|
- 后端在线/离线
|
||||||
|
- 当前加载条数
|
||||||
|
- 当前模式(本地数据 / 真实后端)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 八、数据层设计
|
||||||
|
|
||||||
|
一期不要一开始就完全照搬后端返回结构。
|
||||||
|
你要先定义一个 UE 友好的结构。
|
||||||
|
|
||||||
|
## `S_ComputePoint`
|
||||||
|
|
||||||
|
字段建议:
|
||||||
|
|
||||||
|
- `PointId`:字符串,唯一 ID
|
||||||
|
- `Name`:字符串
|
||||||
|
- `Latitude`:浮点
|
||||||
|
- `Longitude`:浮点
|
||||||
|
- `Performance`:浮点
|
||||||
|
- `CoreCount`:整数
|
||||||
|
- `Country`:字符串
|
||||||
|
- `Source`:字符串
|
||||||
|
|
||||||
|
这个结构同时适用于:
|
||||||
|
|
||||||
|
- 本地 JSON
|
||||||
|
- 后端 API 返回结果转换后的对象
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 九、最稳的执行路线
|
||||||
|
|
||||||
|
下面是整个实施计划最重要的部分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 0:安装和验证环境
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
确保你能:
|
||||||
|
|
||||||
|
- 安装 UE5.4
|
||||||
|
- 启用 Cesium
|
||||||
|
- 能打开一个空项目
|
||||||
|
- 能在 Windows 浏览器访问你的后端
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
满足以下 4 条:
|
||||||
|
|
||||||
|
- UE 能打开
|
||||||
|
- Cesium 能启用
|
||||||
|
- 项目能创建
|
||||||
|
- Windows 浏览器能访问后端 summary 接口
|
||||||
|
|
||||||
|
如果第 4 条做不到,不要继续推进真实接口联调。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 1:创建项目并把地球显示出来
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
打开项目后,能看到一个真实地球。
|
||||||
|
|
||||||
|
## 操作顺序
|
||||||
|
|
||||||
|
1. 新建 UE5 Blank Blueprint 项目
|
||||||
|
2. 创建 `Main` 场景
|
||||||
|
3. 启用 Cesium
|
||||||
|
4. 添加:
|
||||||
|
- `Cesium World Terrain`
|
||||||
|
- `Cesium Sun Sky`
|
||||||
|
- `CesiumGeoreference`
|
||||||
|
5. 调整视角,让你能看到整个地球
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
能录一段短视频,里面能看到地球和镜头移动。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 2:做相机控制
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
让地球可以:
|
||||||
|
|
||||||
|
- 鼠标拖拽旋转
|
||||||
|
- 滚轮缩放
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
这里可以沿用原 MVP 方案的思路:
|
||||||
|
|
||||||
|
- `BP_GlobeCamera` 作为 Pawn
|
||||||
|
- Spring Arm + Camera 组成相机结构
|
||||||
|
- 用输入控制旋转和缩放
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
这一版相机只是“一期可用版”,不是最终镜头系统。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
按 Play 后:
|
||||||
|
|
||||||
|
- 地球可旋转
|
||||||
|
- 可缩放
|
||||||
|
- 不会直接飞走或抖动失控
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3:先喂本地 JSON 数据
|
||||||
|
|
||||||
|
这是融合版方案里最关键的改动。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
不接后端,先验证:
|
||||||
|
|
||||||
|
- 数据结构正常
|
||||||
|
- JSON 能读
|
||||||
|
- 点能生成
|
||||||
|
- 点击交互正常
|
||||||
|
|
||||||
|
## 为什么先这么做
|
||||||
|
因为这样可以把问题收缩成 3 件事:
|
||||||
|
|
||||||
|
- Cesium 坐标转换
|
||||||
|
- 点渲染
|
||||||
|
- UI 弹窗
|
||||||
|
|
||||||
|
不牵涉后端联调。
|
||||||
|
|
||||||
|
## 本地 JSON 示例格式
|
||||||
|
|
||||||
|
建议放在 `Content/Data/compute_points.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"PointId": "top500_1",
|
||||||
|
"Name": "Frontier",
|
||||||
|
"Latitude": 35.93,
|
||||||
|
"Longitude": -84.31,
|
||||||
|
"Performance": 1194.0,
|
||||||
|
"CoreCount": 8730624,
|
||||||
|
"Country": "US",
|
||||||
|
"Source": "top500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"PointId": "top500_2",
|
||||||
|
"Name": "Fugaku",
|
||||||
|
"Latitude": 34.69,
|
||||||
|
"Longitude": 135.19,
|
||||||
|
"Performance": 442.0,
|
||||||
|
"CoreCount": 7630848,
|
||||||
|
"Country": "JP",
|
||||||
|
"Source": "top500"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推荐做法
|
||||||
|
先做一个“本地模式”开关。
|
||||||
|
|
||||||
|
在 `BP_DataLoader` 里支持:
|
||||||
|
|
||||||
|
- Mode = LocalJson
|
||||||
|
- Mode = HttpApi
|
||||||
|
|
||||||
|
先永远跑 `LocalJson`。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
你应该能看到:
|
||||||
|
|
||||||
|
- 多个点出现在地球上
|
||||||
|
- 大致位置正确
|
||||||
|
- 点击能弹信息卡
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 4:做超算点蓝图
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
完成 `BP_ComputePoint`
|
||||||
|
|
||||||
|
每个点要实现:
|
||||||
|
|
||||||
|
- 接收一条 `S_ComputePoint`
|
||||||
|
- 经度纬度转成 UE 世界坐标
|
||||||
|
- 在地球上显示为一个可见的发光球
|
||||||
|
- 支持被点击
|
||||||
|
|
||||||
|
## 显示建议
|
||||||
|
|
||||||
|
### 外观
|
||||||
|
先用最简单的球体 Static Mesh。
|
||||||
|
|
||||||
|
### 材质
|
||||||
|
做一个发光材质:
|
||||||
|
|
||||||
|
- 红橙色
|
||||||
|
- 自发光
|
||||||
|
- 不追求复杂效果
|
||||||
|
|
||||||
|
### 大小
|
||||||
|
球体要足够大,确保在地球尺度下看得见。
|
||||||
|
|
||||||
|
### 高度
|
||||||
|
不要贴地表太近,建议悬浮在地表上方一个固定高度。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
同一批数据点在地球上的位置大体合理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 5:做信息卡
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
点击一个点后,弹出一个简单的信息卡。
|
||||||
|
|
||||||
|
## `WBP_InfoCard` 要显示的内容
|
||||||
|
建议只显示最关键的 3 个字段:
|
||||||
|
|
||||||
|
- 名称
|
||||||
|
- 国家
|
||||||
|
- 算力
|
||||||
|
|
||||||
|
一期先不要堆太多字段。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
点击点 → 卡片出现
|
||||||
|
点击关闭 → 卡片消失
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 6:做基础 HUD
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
屏幕上始终有一个简单状态栏。
|
||||||
|
|
||||||
|
## `WBP_StatusBar` 显示内容建议
|
||||||
|
- 当前模式:Local / HTTP
|
||||||
|
- 已加载数据点数量
|
||||||
|
- 后端状态:Unknown / Online / Offline
|
||||||
|
|
||||||
|
在本地模式阶段,状态可以先写死或显示 `Local Demo`。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
不点击任何点时,屏幕右上角也有“系统正在工作”的感觉。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 7:再接真实后端
|
||||||
|
|
||||||
|
这是第二阶段开始。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
把数据源从本地 JSON 切到 HTTP。
|
||||||
|
|
||||||
|
## 正确做法
|
||||||
|
不要把 `BP_DataLoader` 重写。
|
||||||
|
而是让它支持:
|
||||||
|
|
||||||
|
- LocalJsonLoader
|
||||||
|
- HttpLoader
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
|
||||||
|
**显示层不变,只替换数据来源。**
|
||||||
|
|
||||||
|
## 最重要的接口原则
|
||||||
|
如果后端已有接口字段非常杂,不一定要 UE 直接吃。
|
||||||
|
可以加一个“更适合 UE 的轻量接口”。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
`/api/v1/ue/bootstrap/top500`
|
||||||
|
|
||||||
|
返回尽量扁平的数据:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"PointId": "top500_1",
|
||||||
|
"Name": "Frontier",
|
||||||
|
"Latitude": 35.93,
|
||||||
|
"Longitude": -84.31,
|
||||||
|
"Performance": 1194.0,
|
||||||
|
"CoreCount": 8730624,
|
||||||
|
"Country": "US",
|
||||||
|
"Source": "top500"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 为什么推荐 UE 轻量接口
|
||||||
|
因为 UE 不适合像前端 React 那样,层层解包一大堆复杂 JSON。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 8:做连接状态检测
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
让 HUD 能显示:
|
||||||
|
|
||||||
|
- 在线
|
||||||
|
- 离线
|
||||||
|
- 本地模式
|
||||||
|
|
||||||
|
## 正确实现思路
|
||||||
|
建议用一个很小的状态请求,比如:
|
||||||
|
|
||||||
|
- summary 接口
|
||||||
|
- health 接口
|
||||||
|
- 或 UE 专用 ping 接口
|
||||||
|
|
||||||
|
不要让状态检测去依赖一个超大的数据接口。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
后端关掉时,状态栏能明显变成 Offline。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 9:打包发布
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
把项目打包成 Windows 可执行程序。
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
打包是一期必须尝试的,但不要让它阻塞前面所有开发。
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- 编辑器里没稳定跑通前,不要反复纠结打包
|
||||||
|
- 等 LocalJson 版和 HTTP 版都能在编辑器 Play 模式稳定运行后,再打包
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
双击 exe 可以运行,进入地球场景并正常展示数据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 十、建议的 14 天执行计划
|
||||||
|
|
||||||
|
这版比原 MVP 的时间估计更保守,也更适合新手。
|
||||||
|
|
||||||
|
## 第 1 天
|
||||||
|
- 安装 UE5.4
|
||||||
|
- 安装 Cesium
|
||||||
|
- 创建空项目
|
||||||
|
- 创建 Main 场景
|
||||||
|
|
||||||
|
## 第 2 天
|
||||||
|
- 启用 Cesium
|
||||||
|
- 把地球跑起来
|
||||||
|
- 保存项目结构
|
||||||
|
|
||||||
|
## 第 3 天
|
||||||
|
- 做 `BP_GlobeCamera`
|
||||||
|
- 跑通旋转和缩放
|
||||||
|
|
||||||
|
## 第 4 天
|
||||||
|
- 建 `S_ComputePoint`
|
||||||
|
- 准备本地 JSON 文件
|
||||||
|
- 做 `BP_DataLoader` 的本地模式
|
||||||
|
|
||||||
|
## 第 5 天
|
||||||
|
- 做 `BP_ComputePoint`
|
||||||
|
- 本地 JSON 批量生成点
|
||||||
|
|
||||||
|
## 第 6 天
|
||||||
|
- 调整点大小、颜色、高度
|
||||||
|
- 检查经纬度位置是否大致正确
|
||||||
|
|
||||||
|
## 第 7 天
|
||||||
|
- 做 `WBP_InfoCard`
|
||||||
|
- 跑通点击点弹卡片
|
||||||
|
|
||||||
|
## 第 8 天
|
||||||
|
- 做 `WBP_StatusBar`
|
||||||
|
- 显示本地模式状态和点数量
|
||||||
|
|
||||||
|
## 第 9 天
|
||||||
|
- Windows 浏览器验证后端接口
|
||||||
|
- 准备 HTTP 版加载逻辑
|
||||||
|
|
||||||
|
## 第 10 天
|
||||||
|
- 实现 HTTP 拉真实数据
|
||||||
|
- 先在日志里确认数据到了
|
||||||
|
|
||||||
|
## 第 11 天
|
||||||
|
- 把 HTTP 数据接到点渲染
|
||||||
|
- 切换 Local / HTTP 两种模式
|
||||||
|
|
||||||
|
## 第 12 天
|
||||||
|
- 做连接状态 Online / Offline
|
||||||
|
- 补错误提示
|
||||||
|
|
||||||
|
## 第 13 天
|
||||||
|
- 测试完整链路
|
||||||
|
- 修点选、缩放、HUD 细节
|
||||||
|
|
||||||
|
## 第 14 天
|
||||||
|
- 进行第一次打包
|
||||||
|
- 在 Windows 下运行 exe 验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 十一、这份方案和原 MVP 方案怎么融合
|
||||||
|
|
||||||
|
下面是合并关系。
|
||||||
|
|
||||||
|
## 保留原 MVP 方案的部分
|
||||||
|
这些内容很好,建议继续用:
|
||||||
|
|
||||||
|
- 术语表
|
||||||
|
- Phase 结构化写法
|
||||||
|
- `BP_GlobeCamera`
|
||||||
|
- `BP_ComputePoint`
|
||||||
|
- `WBP_InfoCard`
|
||||||
|
- `WBP_StatusBar`
|
||||||
|
- 相机、点、信息卡、状态栏这 4 个核心对象
|
||||||
|
- “先别做海缆、卫星、BGP”的范围控制
|
||||||
|
|
||||||
|
## 用融合版修正的部分
|
||||||
|
这些是这份新文档加进去的:
|
||||||
|
|
||||||
|
- 两阶段起步:先本地 JSON,再真实后端
|
||||||
|
- 不默认 `localhost` 一定通
|
||||||
|
- 推荐做 UE 轻量接口,而不是死扛原始接口
|
||||||
|
- 把打包放到后段,而不是过早纠结
|
||||||
|
- 时间预估更保守
|
||||||
|
- 明确“一期只是证明链路跑通”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 十二、验收清单
|
||||||
|
|
||||||
|
## 环境
|
||||||
|
- [ ] UE5.4 安装成功
|
||||||
|
- [ ] Cesium 插件启用成功
|
||||||
|
- [ ] Windows 能访问后端接口
|
||||||
|
|
||||||
|
## 本地演示版
|
||||||
|
- [ ] 地球渲染正常
|
||||||
|
- [ ] 鼠标可旋转和缩放
|
||||||
|
- [ ] 本地 JSON 数据能生成点
|
||||||
|
- [ ] 点的位置大体正确
|
||||||
|
- [ ] 点击点能弹信息卡
|
||||||
|
- [ ] HUD 可显示本地模式和点数量
|
||||||
|
|
||||||
|
## 后端接入版
|
||||||
|
- [ ] HTTP 能拉取真实数据
|
||||||
|
- [ ] HTTP 数据能生成点
|
||||||
|
- [ ] HUD 能显示 Online/Offline
|
||||||
|
- [ ] 切换 Local / HTTP 模式不崩
|
||||||
|
- [ ] exe 能打包并运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 十三、后续路线(MVP 之后)
|
||||||
|
|
||||||
|
当这一期做完后,下一步顺序建议是:
|
||||||
|
|
||||||
|
1. 海缆路径
|
||||||
|
2. 卫星点或轨迹
|
||||||
|
3. 更稳的相机与巡航
|
||||||
|
4. WebSocket 增量更新
|
||||||
|
5. BGP 区域态势
|
||||||
|
6. BGP 事件点
|
||||||
|
7. 更强的粒子和视觉风格
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**先补“静态层和镜头层”,再补“高频实时层”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 十四、一句话总结
|
||||||
|
|
||||||
|
这份融合版方案的核心就是:
|
||||||
|
|
||||||
|
**保留原 MVP 的入门友好度,但改成“先本地 JSON、再真实后端”的两阶段实施路线,让你第一次做 UE 时更稳、更容易成功。**
|
||||||
|
|
||||||
|
如果你按这份方案推进,一期最现实的目标不是“立刻做出完整 UE 大屏”,而是:
|
||||||
|
|
||||||
|
**在 14 天左右,做出一个能显示真实地球、能显示超算点、能点击看详情、能接后端的可用 UE 客户端 MVP。**
|
||||||
@@ -16,12 +16,13 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.27.4`
|
- `dev` 当前开发分支历史推导到:`0.27.5`
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
| Version | Type | Branch | Commit | Summary |
|
| Version | Type | Branch | Commit | Summary |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `0.27.5` | bugfix | `dev` | `pending` | 统一控制台自定义滚动条,修复 alerts/BGP 响应式滚动与采集进度完成态显示 |
|
||||||
| `0.27.4` | improvement | `dev` | — | info-card 懒加载动态挂载,页面初始不再有隐藏节点 |
|
| `0.27.4` | improvement | `dev` | — | info-card 懒加载动态挂载,页面初始不再有隐藏节点 |
|
||||||
| `0.27.3` | improvement | `dev` | — | TV panel 折叠方向稳定、视频跳动修复、图例折叠按钮修复、搜索图标调整 |
|
| `0.27.3` | improvement | `dev` | — | TV panel 折叠方向稳定、视频跳动修复、图例折叠按钮修复、搜索图标调整 |
|
||||||
| `0.27.2` | improvement | `dev` | — | 修复 brand copy 宽度问题,提取 --brand-copy-width CSS 变量 |
|
| `0.27.2` | improvement | `dev` | — | 修复 brand copy 宽度问题,提取 --brand-copy-width CSS 变量 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planet-frontend",
|
"name": "planet-frontend",
|
||||||
"version": "0.27.4",
|
"version": "0.27.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1",
|
"packageManager": "bun@1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ function Scrollbar({
|
|||||||
{scrollbar.y.visible ? (
|
{scrollbar.y.visible ? (
|
||||||
<div
|
<div
|
||||||
className="scrollbar__thumb scrollbar__thumb--y"
|
className="scrollbar__thumb scrollbar__thumb--y"
|
||||||
|
tabIndex={0}
|
||||||
style={{
|
style={{
|
||||||
height: `${scrollbar.y.thumbSize}px`,
|
height: `${scrollbar.y.thumbSize}px`,
|
||||||
transform: `translateY(${scrollbar.y.thumbOffset}px)`,
|
transform: `translateY(${scrollbar.y.thumbOffset}px)`,
|
||||||
@@ -284,6 +285,7 @@ function Scrollbar({
|
|||||||
{scrollbar.x.visible ? (
|
{scrollbar.x.visible ? (
|
||||||
<div
|
<div
|
||||||
className="scrollbar__thumb scrollbar__thumb--x"
|
className="scrollbar__thumb scrollbar__thumb--x"
|
||||||
|
tabIndex={0}
|
||||||
style={{
|
style={{
|
||||||
width: `${scrollbar.x.thumbSize}px`,
|
width: `${scrollbar.x.thumbSize}px`,
|
||||||
transform: `translateX(${scrollbar.x.thumbOffset}px)`,
|
transform: `translateX(${scrollbar.x.thumbOffset}px)`,
|
||||||
|
|||||||
37
frontend/src/components/Scrollbar/TableScrollRegion.tsx
Normal file
37
frontend/src/components/Scrollbar/TableScrollRegion.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { forwardRef, useImperativeHandle, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
import ScrollbarOverlay from './ScrollbarOverlay'
|
||||||
|
|
||||||
|
interface TableScrollRegionProps {
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
style?: CSSProperties
|
||||||
|
targetSelector?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableScrollRegion = forwardRef<HTMLDivElement, TableScrollRegionProps>(function TableScrollRegion(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
style,
|
||||||
|
targetSelector = '.ant-table-body',
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => containerRef.current as HTMLDivElement, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={['table-scroll-region', className].filter(Boolean).join(' ')}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ScrollbarOverlay containerRef={containerRef} targetSelector={targetSelector} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default TableScrollRegion
|
||||||
@@ -140,7 +140,8 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transition: opacity 0.18s ease, background 0.18s ease;
|
outline: none;
|
||||||
|
transition: opacity 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar__thumb::after {
|
.scrollbar__thumb::after {
|
||||||
@@ -173,6 +174,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar:hover .scrollbar__track--visible .scrollbar__thumb,
|
.scrollbar:hover .scrollbar__track--visible .scrollbar__thumb,
|
||||||
|
.scrollbar:focus-within .scrollbar__track--visible .scrollbar__thumb,
|
||||||
.scrollbar__track--visible .scrollbar__thumb,
|
.scrollbar__track--visible .scrollbar__thumb,
|
||||||
.scrollbar__track--dragging .scrollbar__thumb {
|
.scrollbar__track--dragging .scrollbar__thumb {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -182,12 +184,15 @@ body {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar__thumb:hover {
|
.scrollbar__thumb:hover,
|
||||||
background: rgba(216, 226, 240, 0.68);
|
.scrollbar__thumb:focus-visible {
|
||||||
|
background: rgba(125, 146, 174, 0.82);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar__track--dragging .scrollbar__thumb {
|
.scrollbar__track--dragging .scrollbar__thumb {
|
||||||
background: rgba(226, 235, 246, 0.82);
|
background: rgba(92, 115, 146, 0.9);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-brand {
|
.dashboard-brand {
|
||||||
@@ -398,9 +403,11 @@ body {
|
|||||||
|
|
||||||
.playground-card__scroll {
|
.playground-card__scroll {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-card__scroll .scrollbar__viewport {
|
||||||
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-card__scroll::-webkit-scrollbar,
|
.playground-card__scroll::-webkit-scrollbar,
|
||||||
@@ -1251,25 +1258,21 @@ body {
|
|||||||
.playground-result-modal__content {
|
.playground-result-modal__content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-result-modal__content::-webkit-scrollbar {
|
.playground-result-modal__content .scrollbar__viewport {
|
||||||
width: 8px;
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-result-modal__content::-webkit-scrollbar-thumb {
|
.playground-result__blocks-scroll .scrollbar__viewport {
|
||||||
background: rgba(148, 163, 184, 0.82);
|
height: 100%;
|
||||||
border-radius: 999px;
|
overflow: auto;
|
||||||
border: 2px solid transparent;
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-result-modal__content::-webkit-scrollbar-track {
|
.playground-result__blocks-scroll.scrollbar {
|
||||||
background: transparent;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
@@ -1583,14 +1586,14 @@ body {
|
|||||||
padding: 10px 12px !important;
|
padding: 10px 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-source-table-region .ant-table-body,
|
.table-scroll-region .ant-table-body,
|
||||||
.data-source-table-region .ant-table-content {
|
.table-scroll-region .ant-table-content {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-source-table-region .ant-table-body::-webkit-scrollbar,
|
.table-scroll-region .ant-table-body::-webkit-scrollbar,
|
||||||
.data-source-table-region .ant-table-content::-webkit-scrollbar {
|
.table-scroll-region .ant-table-content::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
@@ -1757,7 +1760,10 @@ body {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||||
@@ -1836,10 +1842,11 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
max-height: calc(100vh - 180px);
|
max-height: calc(100vh - 180px);
|
||||||
overflow: auto;
|
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
scrollbar-width: thin;
|
}
|
||||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
|
||||||
|
.bgp-page__brief-modal-body .scrollbar__viewport {
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bgp-page__brief-evidence {
|
.bgp-page__brief-evidence {
|
||||||
@@ -1857,6 +1864,10 @@ body {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alerts-brief-drawer .scrollbar__viewport {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.alerts-brief-drawer__loading {
|
.alerts-brief-drawer__loading {
|
||||||
min-height: 160px;
|
min-height: 160px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1887,8 +1898,17 @@ body {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-tab-panel > .ant-space-item,
|
||||||
|
.system-alerts-page__stack > .ant-space-item,
|
||||||
|
.bgp-alerts-page__stack > .ant-space-item,
|
||||||
|
.situational-alerts-page__stack > .ant-space-item {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-alerts-page__table-card,
|
.system-alerts-page__table-card,
|
||||||
@@ -2128,29 +2148,71 @@ body {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bgp-page__summary-grid--compact {
|
.bgp-page__summary-scroll.scrollbar,
|
||||||
flex-wrap: nowrap !important;
|
.alerts-summary-scroll.scrollbar {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgp-page__summary-scroll .scrollbar__viewport,
|
||||||
|
.alerts-summary-scroll .scrollbar__viewport {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(148, 163, 184, 0.82) transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bgp-page__summary-grid--compact::-webkit-scrollbar {
|
.bgp-page__summary-grid,
|
||||||
width: 8px;
|
.alerts-summary-grid {
|
||||||
height: 8px;
|
display: flex;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bgp-page__summary-grid--compact::-webkit-scrollbar-thumb {
|
.bgp-page__summary-cell,
|
||||||
background: rgba(148, 163, 184, 0.82);
|
.alerts-summary-cell {
|
||||||
border-radius: 999px;
|
flex: 0 0 auto;
|
||||||
border: 2px solid transparent;
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bgp-page__summary-grid--compact::-webkit-scrollbar-track {
|
.situational-alerts-page__summary-grid {
|
||||||
background: transparent;
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.situational-alerts-page__panels-grid {
|
||||||
|
display: flex;
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.situational-alerts-page__panels-scroll .scrollbar__viewport {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.situational-alerts-page__summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.situational-alerts-page__summary-grid .alerts-summary-cell {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.situational-alerts-page__summary-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: auto;
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.situational-alerts-page__summary-grid .alerts-summary-cell {
|
||||||
|
width: 220px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bgp-page__table-card,
|
.bgp-page__table-card,
|
||||||
@@ -2408,25 +2470,21 @@ body {
|
|||||||
.settings-panel-scroll {
|
.settings-panel-scroll {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel-scroll.scrollbar {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel-scroll .scrollbar__viewport {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-right: 6px;
|
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-panel-scroll::-webkit-scrollbar {
|
.settings-panel-scroll .scrollbar__viewport > * {
|
||||||
width: 10px;
|
min-width: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.settings-panel-scroll::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(148, 163, 184, 0.8);
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-panel-scroll::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-pane .data-source-table-region .ant-table-container {
|
.settings-pane .data-source-table-region .ant-table-container {
|
||||||
@@ -3273,7 +3331,6 @@ body {
|
|||||||
|
|
||||||
.dashboard-restart-log {
|
.dashboard-restart-log {
|
||||||
max-height: 180px;
|
max-height: 180px;
|
||||||
overflow-y: auto;
|
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
@@ -3281,16 +3338,11 @@ body {
|
|||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-restart-log::-webkit-scrollbar {
|
.dashboard-restart-log .scrollbar__viewport {
|
||||||
width: 8px;
|
overflow-y: auto;
|
||||||
}
|
max-height: 180px;
|
||||||
|
|
||||||
.dashboard-restart-log::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(148, 163, 184, 0.55);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Col,
|
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Modal,
|
Modal,
|
||||||
Row,
|
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Statistic,
|
Statistic,
|
||||||
@@ -21,6 +19,8 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
|
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||||
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
import type { BGPAnomaly, BGPBriefRecord, BGPIncident } from '../../services/situational-awareness'
|
import type { BGPAnomaly, BGPBriefRecord, BGPIncident } from '../../services/situational-awareness'
|
||||||
import { getSituationalAwarenessGateway } from '../../services/situational-awareness'
|
import { getSituationalAwarenessGateway } from '../../services/situational-awareness'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
@@ -171,20 +171,22 @@ export function BGPAlertsPanel() {
|
|||||||
|
|
||||||
<Alert type="info" showIcon message="这里聚焦 BGP 风险信号本身,不等同于系统平台运行告警。" />
|
<Alert type="info" showIcon message="这里聚焦 BGP 风险信号本身,不等同于系统平台运行告警。" />
|
||||||
|
|
||||||
<Row gutter={[12, 12]}>
|
<Scrollbar className="alerts-summary-scroll bgp-alerts-page__summary-scroll">
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<div className="alerts-summary-grid" style={{ gap: '12px' }}>
|
||||||
<Card><Statistic title="活跃事件" value={summary.activeIncidents} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="活跃事件" value={summary.activeIncidents} /></Card>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
</div>
|
||||||
<Card><Statistic title="严重事件" value={summary.criticalIncidents} valueStyle={{ color: '#ff4d4f' }} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="严重事件" value={summary.criticalIncidents} valueStyle={{ color: '#ff4d4f' }} /></Card>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
</div>
|
||||||
<Card><Statistic title="活跃异常" value={summary.activeAnomalies} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="活跃异常" value={summary.activeAnomalies} /></Card>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
</div>
|
||||||
<Card><Statistic title="高风险异常" value={summary.highRiskAnomalies} valueStyle={{ color: '#fa8c16' }} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="高风险异常" value={summary.highRiskAnomalies} valueStyle={{ color: '#fa8c16' }} /></Card>
|
||||||
</Row>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Scrollbar>
|
||||||
|
|
||||||
<Card className="bgp-alerts-page__table-card">
|
<Card className="bgp-alerts-page__table-card">
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -194,7 +196,7 @@ export function BGPAlertsPanel() {
|
|||||||
key: 'incidents',
|
key: 'incidents',
|
||||||
label: 'BGP 事件',
|
label: 'BGP 事件',
|
||||||
children: (
|
children: (
|
||||||
<div className="table-scroll-region bgp-alerts-page__table-region">
|
<TableScrollRegion className="bgp-alerts-page__table-region">
|
||||||
<Table<BGPIncident>
|
<Table<BGPIncident>
|
||||||
columns={incidentColumns}
|
columns={incidentColumns}
|
||||||
dataSource={incidents}
|
dataSource={incidents}
|
||||||
@@ -204,14 +206,14 @@ export function BGPAlertsPanel() {
|
|||||||
scroll={{ x: 1200, y: 480 }}
|
scroll={{ x: 1200, y: 480 }}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'anomalies',
|
key: 'anomalies',
|
||||||
label: 'BGP 异常',
|
label: 'BGP 异常',
|
||||||
children: (
|
children: (
|
||||||
<div className="table-scroll-region bgp-alerts-page__table-region">
|
<TableScrollRegion className="bgp-alerts-page__table-region">
|
||||||
<Table<BGPAnomaly>
|
<Table<BGPAnomaly>
|
||||||
columns={anomalyColumns}
|
columns={anomalyColumns}
|
||||||
dataSource={anomalies}
|
dataSource={anomalies}
|
||||||
@@ -221,7 +223,7 @@ export function BGPAlertsPanel() {
|
|||||||
scroll={{ x: 1100, y: 480 }}
|
scroll={{ x: 1100, y: 480 }}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -244,14 +246,14 @@ export function BGPAlertsPanel() {
|
|||||||
<Spin tip="正在生成 BGP AI 简报..." />
|
<Spin tip="正在生成 BGP AI 简报..." />
|
||||||
</div>
|
</div>
|
||||||
) : brief ? (
|
) : brief ? (
|
||||||
<div className="bgp-page__brief-modal-body">
|
<Scrollbar className="bgp-page__brief-modal-body">
|
||||||
<Descriptions size="small" column={3} className="bgp-page__brief-meta">
|
<Descriptions size="small" column={3} className="bgp-page__brief-meta">
|
||||||
<Descriptions.Item label="Provider">{brief.provider || '-'}</Descriptions.Item>
|
<Descriptions.Item label="Provider">{brief.provider || '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="模型">{brief.model || '-'}</Descriptions.Item>
|
<Descriptions.Item label="模型">{brief.model || '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="生成时间">{formatDateTimeZhCN(brief.generated_at)}</Descriptions.Item>
|
<Descriptions.Item label="生成时间">{formatDateTimeZhCN(brief.generated_at)}</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
<Typography.Paragraph className="alerts-brief-content">{brief.content_markdown}</Typography.Paragraph>
|
<Typography.Paragraph className="alerts-brief-content">{brief.content_markdown}</Typography.Paragraph>
|
||||||
</div>
|
</Scrollbar>
|
||||||
) : (
|
) : (
|
||||||
<Text type="secondary">当前没有可查看的 BGP 简报。</Text>
|
<Text type="secondary">当前没有可查看的 BGP 简报。</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Col,
|
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Row,
|
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Statistic,
|
Statistic,
|
||||||
@@ -18,6 +16,7 @@ import {
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||||
import type { BGPSummarySnapshot } from '../../services/situational-awareness'
|
import type { BGPSummarySnapshot } from '../../services/situational-awareness'
|
||||||
import {
|
import {
|
||||||
getSituationalAwarenessGateway,
|
getSituationalAwarenessGateway,
|
||||||
@@ -119,42 +118,46 @@ export function SituationalAlertsPanel() {
|
|||||||
message="态势告警不是单一模块列表,而是把系统告警与 BGP 风险综合成一份值班研判入口。"
|
message="态势告警不是单一模块列表,而是把系统告警与 BGP 风险综合成一份值班研判入口。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Row gutter={[12, 12]}>
|
<Scrollbar className="alerts-summary-scroll situational-alerts-page__summary-scroll">
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<div className="alerts-summary-grid situational-alerts-page__summary-grid" style={{ gap: '12px' }}>
|
||||||
<Card><Statistic title="活跃系统告警" value={summary.activeSystemAlerts} prefix={<WarningOutlined />} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="活跃系统告警" value={summary.activeSystemAlerts} prefix={<WarningOutlined />} /></Card>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
</div>
|
||||||
<Card><Statistic title="严重系统告警" value={summary.criticalSystemAlerts} valueStyle={{ color: '#ff4d4f' }} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="严重系统告警" value={summary.criticalSystemAlerts} valueStyle={{ color: '#ff4d4f' }} /></Card>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
</div>
|
||||||
<Card><Statistic title="活跃 BGP 事件" value={summary.activeBGPIncidents} prefix={<DeploymentUnitOutlined />} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="活跃 BGP 事件" value={summary.activeBGPIncidents} prefix={<DeploymentUnitOutlined />} /></Card>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
</div>
|
||||||
<Card><Statistic title="严重 BGP 事件" value={summary.criticalBGPIncidents} valueStyle={{ color: '#fa8c16' }} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="严重 BGP 事件" value={summary.criticalBGPIncidents} valueStyle={{ color: '#fa8c16' }} /></Card>
|
||||||
</Row>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Scrollbar>
|
||||||
|
|
||||||
<Row gutter={[12, 12]}>
|
<Scrollbar className="alerts-summary-scroll situational-alerts-page__panels-scroll">
|
||||||
<Col xs={24} lg={12}>
|
<div className="alerts-summary-grid situational-alerts-page__panels-grid" style={{ gap: '12px' }}>
|
||||||
<Card title="系统告警侧">
|
<div className="alerts-summary-cell" style={{ width: '360px' }}>
|
||||||
<Descriptions size="small" column={1}>
|
<Card title="系统告警侧">
|
||||||
<Descriptions.Item label="严重">{String(systemStats?.critical ?? '-')}</Descriptions.Item>
|
<Descriptions size="small" column={1}>
|
||||||
<Descriptions.Item label="警告">{String(systemStats?.warning ?? '-')}</Descriptions.Item>
|
<Descriptions.Item label="严重">{String(systemStats?.critical ?? '-')}</Descriptions.Item>
|
||||||
<Descriptions.Item label="信息">{String(systemStats?.info ?? '-')}</Descriptions.Item>
|
<Descriptions.Item label="警告">{String(systemStats?.warning ?? '-')}</Descriptions.Item>
|
||||||
</Descriptions>
|
<Descriptions.Item label="信息">{String(systemStats?.info ?? '-')}</Descriptions.Item>
|
||||||
</Card>
|
</Descriptions>
|
||||||
</Col>
|
</Card>
|
||||||
<Col xs={24} lg={12}>
|
</div>
|
||||||
<Card title="BGP 风险侧">
|
<div className="alerts-summary-cell" style={{ width: '360px' }}>
|
||||||
<Descriptions size="small" column={1}>
|
<Card title="BGP 风险侧">
|
||||||
<Descriptions.Item label="活跃事件">{String(bgpSummary?.incidentSummary?.by_status?.active ?? '-')}</Descriptions.Item>
|
<Descriptions size="small" column={1}>
|
||||||
<Descriptions.Item label="严重事件">{String(bgpSummary?.incidentSummary?.by_severity?.critical ?? '-')}</Descriptions.Item>
|
<Descriptions.Item label="活跃事件">{String(bgpSummary?.incidentSummary?.by_status?.active ?? '-')}</Descriptions.Item>
|
||||||
<Descriptions.Item label="活跃观测站">{String(bgpSummary?.collectorSummary?.active_collectors ?? '-')}</Descriptions.Item>
|
<Descriptions.Item label="严重事件">{String(bgpSummary?.incidentSummary?.by_severity?.critical ?? '-')}</Descriptions.Item>
|
||||||
<Descriptions.Item label="近24h事件">{String(bgpSummary?.collectorSummary?.recent_24h_events ?? '-')}</Descriptions.Item>
|
<Descriptions.Item label="活跃观测站">{String(bgpSummary?.collectorSummary?.active_collectors ?? '-')}</Descriptions.Item>
|
||||||
</Descriptions>
|
<Descriptions.Item label="近24h事件">{String(bgpSummary?.collectorSummary?.recent_24h_events ?? '-')}</Descriptions.Item>
|
||||||
</Card>
|
</Descriptions>
|
||||||
</Col>
|
</Card>
|
||||||
</Row>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Scrollbar>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Drawer title="态势告警 AI 简报" placement="right" width={560} onClose={() => setBriefOpen(false)} open={briefOpen}>
|
<Drawer title="态势告警 AI 简报" placement="right" width={560} onClose={() => setBriefOpen(false)} open={briefOpen}>
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Col,
|
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Modal,
|
Modal,
|
||||||
Row,
|
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Statistic,
|
Statistic,
|
||||||
@@ -22,6 +20,8 @@ import {
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||||
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
import type { AlertBriefResponse, AlertRecord } from '../../services/situational-awareness'
|
import type { AlertBriefResponse, AlertRecord } from '../../services/situational-awareness'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
|
|
||||||
@@ -216,20 +216,22 @@ export function SystemAlertsPanel() {
|
|||||||
|
|
||||||
<Alert type="info" showIcon message="这里展示的是平台与采集链路告警,不等同于 BGP 态势风险本身。" />
|
<Alert type="info" showIcon message="这里展示的是平台与采集链路告警,不等同于 BGP 态势风险本身。" />
|
||||||
|
|
||||||
<Row gutter={[12, 12]}>
|
<Scrollbar className="alerts-summary-scroll system-alerts-page__summary-scroll">
|
||||||
<Col xs={24} sm={8}>
|
<div className="alerts-summary-grid" style={{ gap: '12px' }}>
|
||||||
<Card><Statistic title="严重告警" value={stats.critical} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="严重告警" value={stats.critical} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} /></Card>
|
||||||
<Col xs={24} sm={8}>
|
</div>
|
||||||
<Card><Statistic title="警告" value={stats.warning} valueStyle={{ color: '#faad14' }} prefix={<AlertOutlined />} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="警告" value={stats.warning} valueStyle={{ color: '#faad14' }} prefix={<AlertOutlined />} /></Card>
|
||||||
<Col xs={24} sm={8}>
|
</div>
|
||||||
<Card><Statistic title="信息" value={stats.info} valueStyle={{ color: '#1890ff' }} prefix={<InfoCircleOutlined />} /></Card>
|
<div className="alerts-summary-cell" style={{ width: '220px' }}>
|
||||||
</Col>
|
<Card><Statistic title="信息" value={stats.info} valueStyle={{ color: '#1890ff' }} prefix={<InfoCircleOutlined />} /></Card>
|
||||||
</Row>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Scrollbar>
|
||||||
|
|
||||||
<Card className="system-alerts-page__table-card" title="系统告警列表">
|
<Card className="system-alerts-page__table-card" title="系统告警列表">
|
||||||
<div className="table-scroll-region system-alerts-page__table-region">
|
<TableScrollRegion className="system-alerts-page__table-region">
|
||||||
<Table<AlertRecord>
|
<Table<AlertRecord>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={alerts}
|
dataSource={alerts}
|
||||||
@@ -239,7 +241,7 @@ export function SystemAlertsPanel() {
|
|||||||
scroll={{ x: 1100, y: 480 }}
|
scroll={{ x: 1100, y: 480 }}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
@@ -260,7 +262,7 @@ export function SystemAlertsPanel() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Drawer title="系统告警 AI 简报" placement="right" width={520} onClose={() => setBriefOpen(false)} open={briefOpen}>
|
<Drawer title="系统告警 AI 简报" placement="right" width={520} onClose={() => setBriefOpen(false)} open={briefOpen}>
|
||||||
<div className="alerts-brief-drawer">
|
<Scrollbar className="alerts-brief-drawer">
|
||||||
{briefLoading ? (
|
{briefLoading ? (
|
||||||
<div className="alerts-brief-drawer__loading">
|
<div className="alerts-brief-drawer__loading">
|
||||||
<Spin tip="正在汇总系统告警事实并生成简报..." />
|
<Spin tip="正在汇总系统告警事实并生成简报..." />
|
||||||
@@ -293,7 +295,7 @@ export function SystemAlertsPanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</Scrollbar>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Col,
|
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Modal,
|
Modal,
|
||||||
Row,
|
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
@@ -22,6 +20,8 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
|
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
|
||||||
|
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||||
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
import {
|
import {
|
||||||
type BGPAnomaly,
|
type BGPAnomaly,
|
||||||
@@ -708,37 +708,36 @@ function BGP() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="bgp-page__summary-card">
|
<Card className="bgp-page__summary-card">
|
||||||
<Row
|
<Scrollbar className="bgp-page__summary-scroll">
|
||||||
gutter={[compactViewport ? 8 : 12, compactViewport ? 8 : 12]}
|
<div
|
||||||
className={`bgp-page__summary-grid${compactViewport ? ' bgp-page__summary-grid--compact' : ''}`}
|
className="bgp-page__summary-grid"
|
||||||
wrap={!compactViewport}
|
style={{ gap: `${compactViewport ? 8 : 12}px` }}
|
||||||
>
|
>
|
||||||
{summaryItems.map((item) => (
|
{summaryItems.map((item) => (
|
||||||
<Col
|
<div
|
||||||
key={item.label}
|
key={item.label}
|
||||||
xs={24}
|
className="bgp-page__summary-cell"
|
||||||
sm={12}
|
style={{ width: compactViewport ? '180px' : '220px' }}
|
||||||
md={8}
|
>
|
||||||
flex={compactViewport ? '180px' : undefined}
|
<div className="bgp-page__summary-item">
|
||||||
>
|
<div className="bgp-page__summary-label">{item.label}</div>
|
||||||
<div className="bgp-page__summary-item">
|
<Statistic className="bgp-page__summary-stat" value={item.value} />
|
||||||
<div className="bgp-page__summary-label">{item.label}</div>
|
</div>
|
||||||
<Statistic className="bgp-page__summary-stat" value={item.value} />
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
))}
|
||||||
))}
|
</div>
|
||||||
</Row>
|
</Scrollbar>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bgp-page__table-card">
|
<Card className="bgp-page__table-card">
|
||||||
<div ref={tableRegionRef} className="table-scroll-region bgp-page__table-region">
|
<TableScrollRegion ref={tableRegionRef} className="bgp-page__table-region">
|
||||||
<Tabs
|
<Tabs
|
||||||
className="bgp-page__tabs"
|
className="bgp-page__tabs"
|
||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
items={tabItems}
|
items={tabItems}
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
@@ -755,7 +754,7 @@ function BGP() {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
{brief ? (
|
{brief ? (
|
||||||
<div className="bgp-page__brief-modal-body">
|
<Scrollbar className="bgp-page__brief-modal-body">
|
||||||
{(brief.facts.length > 0 || Object.keys(brief.context || {}).length > 0) ? (
|
{(brief.facts.length > 0 || Object.keys(brief.context || {}).length > 0) ? (
|
||||||
<div className="bgp-page__brief-evidence">
|
<div className="bgp-page__brief-evidence">
|
||||||
{brief.facts.length > 0 ? (
|
{brief.facts.length > 0 ? (
|
||||||
@@ -785,7 +784,7 @@ function BGP() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<MarkdownRenderer markdown={brief.content_markdown} />
|
<MarkdownRenderer markdown={brief.content_markdown} />
|
||||||
</div>
|
</Scrollbar>
|
||||||
) : (
|
) : (
|
||||||
<Text type="secondary">当前没有可查看的简报内容。</Text>
|
<Text type="secondary">当前没有可查看的简报内容。</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Link } from 'react-router-dom'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||||
import { useWebSocket } from '../../hooks/useWebSocket'
|
import { useWebSocket } from '../../hooks/useWebSocket'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
|
|
||||||
@@ -532,11 +533,11 @@ function Dashboard() {
|
|||||||
|
|
||||||
<div className="dashboard-restart-section">
|
<div className="dashboard-restart-section">
|
||||||
<Text className="dashboard-restart-section__label">终端输出</Text>
|
<Text className="dashboard-restart-section__label">终端输出</Text>
|
||||||
<div className="dashboard-restart-log">
|
<Scrollbar className="dashboard-restart-log">
|
||||||
{restartLogs.length > 0 ? restartLogs.map((line, index) => (
|
{restartLogs.length > 0 ? restartLogs.map((line, index) => (
|
||||||
<div key={`${line}-${index}`}>{line}</div>
|
<div key={`${line}-${index}`}>{line}</div>
|
||||||
)) : <div>等待操作</div>}
|
)) : <div>等待操作</div>}
|
||||||
</div>
|
</Scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
import { formatDateTimeZhCN, formatDateZhCN, parseBackendDate } from '../../utils/datetime'
|
import { formatDateTimeZhCN, formatDateZhCN, parseBackendDate } from '../../utils/datetime'
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Title, Text } = Typography
|
||||||
@@ -939,7 +940,7 @@ function DataList() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-scroll-region data-list-table-region" style={{ padding: isCompact ? 10 : 12 }}>
|
<TableScrollRegion className="data-list-table-region" style={{ padding: isCompact ? 10 : 12 }}>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
@@ -960,7 +961,7 @@ function DataList() {
|
|||||||
showTotal: (count) => `共 ${count} 条`,
|
showTotal: (count) => `共 ${count} 条`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -116,18 +116,39 @@ function finalizeBulkProgressBatch(batch: BulkProgressBatch | null): BulkProgres
|
|||||||
if (!batch || batch.sourceIds.length === 0) {
|
if (!batch || batch.sourceIds.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
return batch
|
||||||
|
}
|
||||||
|
|
||||||
const hasRunningItem = batch.sourceIds.some((sourceId) => batch.items[sourceId]?.is_running)
|
function resolveTerminalBatchItem(
|
||||||
if (hasRunningItem) {
|
sourceId: number,
|
||||||
return batch
|
batch: BulkProgressBatch,
|
||||||
|
builtInSources: BuiltInDataSource[],
|
||||||
|
taskProgress: Record<number, TaskTrackerState>,
|
||||||
|
): BulkProgressItem | null {
|
||||||
|
const currentItem = batch.items[sourceId]
|
||||||
|
const source = builtInSources.find((item) => item.id === sourceId)
|
||||||
|
const trackedTask = taskProgress[sourceId]
|
||||||
|
|
||||||
|
const isRunning = trackedTask?.is_running ?? source?.is_running ?? currentItem?.is_running ?? false
|
||||||
|
if (isRunning) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const allFinished = batch.sourceIds.every((sourceId) => {
|
const status = trackedTask?.status ?? source?.last_status ?? currentItem?.status ?? null
|
||||||
const status = batch.items[sourceId]?.status
|
if (!status || status === 'running') {
|
||||||
return Boolean(status && status !== 'running')
|
return null
|
||||||
})
|
}
|
||||||
|
|
||||||
return allFinished ? null : batch
|
return {
|
||||||
|
task_id: trackedTask?.task_id ?? currentItem?.task_id ?? source?.task_id ?? null,
|
||||||
|
progress:
|
||||||
|
status === 'success'
|
||||||
|
? 100
|
||||||
|
: trackedTask?.progress ?? source?.progress ?? currentItem?.progress ?? 0,
|
||||||
|
is_running: false,
|
||||||
|
phase: trackedTask?.phase ?? source?.phase ?? currentItem?.phase ?? null,
|
||||||
|
status,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSocketTaskMessage {
|
interface WebSocketTaskMessage {
|
||||||
@@ -436,6 +457,41 @@ function DataSources() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [builtInSources, taskProgress, taskSocketConnected, fetchData])
|
}, [builtInSources, taskProgress, taskSocketConnected, fetchData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!bulkProgressBatch) return
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
const nextItems = { ...bulkProgressBatch.items }
|
||||||
|
|
||||||
|
for (const sourceId of bulkProgressBatch.sourceIds) {
|
||||||
|
const nextItem = resolveTerminalBatchItem(sourceId, bulkProgressBatch, builtInSources, taskProgress)
|
||||||
|
if (!nextItem) continue
|
||||||
|
|
||||||
|
const previousItem = bulkProgressBatch.items[sourceId]
|
||||||
|
if (
|
||||||
|
previousItem?.status === nextItem.status &&
|
||||||
|
previousItem?.is_running === nextItem.is_running &&
|
||||||
|
previousItem?.progress === nextItem.progress &&
|
||||||
|
previousItem?.phase === nextItem.phase
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nextItems[sourceId] = nextItem
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return
|
||||||
|
|
||||||
|
setBulkProgressBatch((prev) => {
|
||||||
|
if (!prev) return prev
|
||||||
|
return finalizeBulkProgressBatch({
|
||||||
|
...prev,
|
||||||
|
items: nextItems,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [bulkProgressBatch, builtInSources, taskProgress])
|
||||||
|
|
||||||
const triggerDatasource = async (id: number, options?: { force?: boolean }) => {
|
const triggerDatasource = async (id: number, options?: { force?: boolean }) => {
|
||||||
const force = options?.force ?? false
|
const force = options?.force ?? false
|
||||||
const res = await axios.post(`/api/v1/datasources/${id}/trigger`, null, {
|
const res = await axios.post(`/api/v1/datasources/${id}/trigger`, null, {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import axios from 'axios'
|
|||||||
|
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
|
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
|
||||||
|
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||||
|
import ScrollbarOverlay from '../../components/Scrollbar/ScrollbarOverlay'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography
|
const { Title, Text, Paragraph } = Typography
|
||||||
@@ -239,6 +241,7 @@ function Playground() {
|
|||||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||||
const pollTimerRef = useRef<number | null>(null)
|
const pollTimerRef = useRef<number | null>(null)
|
||||||
const messagesContainerRef = useRef<HTMLDivElement | null>(null)
|
const messagesContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const messagesShellRef = useRef<HTMLDivElement | null>(null)
|
||||||
const forceScrollToBottomRef = useRef(true)
|
const forceScrollToBottomRef = useRef(true)
|
||||||
|
|
||||||
const selectedPreset = useMemo(
|
const selectedPreset = useMemo(
|
||||||
@@ -575,7 +578,7 @@ function Playground() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="playground-card__scroll">
|
<Scrollbar className="playground-card__scroll">
|
||||||
<Spin spinning={statusLoading}>
|
<Spin spinning={statusLoading}>
|
||||||
{providerStatus ? (
|
{providerStatus ? (
|
||||||
<div className="playground-provider-panel">
|
<div className="playground-provider-panel">
|
||||||
@@ -617,7 +620,7 @@ function Playground() {
|
|||||||
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
|
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
|
||||||
)}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</Scrollbar>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -699,7 +702,7 @@ function Playground() {
|
|||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="playground-chat__messages-shell">
|
<div ref={messagesShellRef} className="playground-chat__messages-shell">
|
||||||
<div className="playground-chat__messages" ref={messagesContainerRef} onScroll={handleMessagesScroll}>
|
<div className="playground-chat__messages" ref={messagesContainerRef} onScroll={handleMessagesScroll}>
|
||||||
{messages.map((entry) => (
|
{messages.map((entry) => (
|
||||||
<div key={entry.id} className={`playground-message playground-message--${entry.role}`}>
|
<div key={entry.id} className={`playground-message playground-message--${entry.role}`}>
|
||||||
@@ -760,8 +763,9 @@ function Playground() {
|
|||||||
<Button size="small" type="primary" loading={editSaving} onClick={() => void handleSaveEditMessage(entry)}>
|
<Button size="small" type="primary" loading={editSaving} onClick={() => void handleSaveEditMessage(entry)}>
|
||||||
发送
|
发送
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ScrollbarOverlay containerRef={messagesShellRef} targetSelector=".playground-chat__messages" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (entry.role !== 'assistant' || entry.status === 'answering' || entry.status === 'done' || entry.status === 'stopped' || entry.status === 'error') && entry.markdown ? (
|
) : (entry.role !== 'assistant' || entry.status === 'answering' || entry.status === 'done' || entry.status === 'stopped' || entry.status === 'error') && entry.markdown ? (
|
||||||
<div className="playground-message__markdown">
|
<div className="playground-message__markdown">
|
||||||
@@ -1000,42 +1004,42 @@ function Playground() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="playground-result-modal__content">
|
<Scrollbar className="playground-result-modal__content">
|
||||||
<Space direction="vertical" size={16} className="playground-result__stack" style={{ width: '100%' }}>
|
<Space direction="vertical" size={16} className="playground-result__stack" style={{ width: '100%' }}>
|
||||||
{analysis.text_blocks.length ? (
|
{analysis.text_blocks.length ? (
|
||||||
<div className="playground-result__blocks">
|
<div className="playground-result__blocks">
|
||||||
<Text strong>文本块</Text>
|
<Text strong>文本块</Text>
|
||||||
<div className="playground-result__blocks-scroll">
|
<Scrollbar className="playground-result__blocks-scroll">
|
||||||
{analysis.text_blocks.map((block, index) => (
|
{analysis.text_blocks.map((block, index) => (
|
||||||
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
|
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
|
||||||
<pre>{block}</pre>
|
<pre>{block}</pre>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Scrollbar>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{analysis.thinking_blocks.length ? (
|
{analysis.thinking_blocks.length ? (
|
||||||
<div className="playground-result__blocks">
|
<div className="playground-result__blocks">
|
||||||
<Text strong>Thinking Blocks</Text>
|
<Text strong>Thinking Blocks</Text>
|
||||||
<div className="playground-result__blocks-scroll">
|
<Scrollbar className="playground-result__blocks-scroll">
|
||||||
{analysis.thinking_blocks.map((block, index) => (
|
{analysis.thinking_blocks.map((block, index) => (
|
||||||
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
|
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
|
||||||
<pre>{block}</pre>
|
<pre>{block}</pre>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Scrollbar>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="playground-result__blocks">
|
<div className="playground-result__blocks">
|
||||||
<Text strong>Raw Response</Text>
|
<Text strong>Raw Response</Text>
|
||||||
<div className="playground-result__blocks-scroll playground-result__blocks-scroll--raw">
|
<Scrollbar className="playground-result__blocks-scroll playground-result__blocks-scroll--raw">
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<pre>{JSON.stringify(analysis.raw_response, null, 2)}</pre>
|
<pre>{JSON.stringify(analysis.raw_response, null, 2)}</pre>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</Scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</Scrollbar>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||||
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Title, Text } = Typography
|
||||||
@@ -91,7 +93,7 @@ function SettingsPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="settings-pane">
|
<div className="settings-pane">
|
||||||
<Card className="settings-panel-card" loading={loading}>
|
<Card className="settings-panel-card" loading={loading}>
|
||||||
<div className="settings-panel-scroll">{children}</div>
|
<Scrollbar className="settings-panel-scroll">{children}</Scrollbar>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -642,7 +644,7 @@ function Settings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-scroll-region data-source-table-region">
|
<TableScrollRegion className="data-source-table-region">
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={tvSourceColumns}
|
columns={tvSourceColumns}
|
||||||
@@ -652,7 +654,7 @@ function Settings() {
|
|||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,7 +670,7 @@ function Settings() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
styles={{ body: { padding: 0 } }}
|
styles={{ body: { padding: 0 } }}
|
||||||
>
|
>
|
||||||
<div ref={collectorTableRegionRef} className="table-scroll-region data-source-table-region">
|
<TableScrollRegion ref={collectorTableRegionRef} className="data-source-table-region">
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={collectorColumns}
|
columns={collectorColumns}
|
||||||
@@ -678,7 +680,7 @@ function Settings() {
|
|||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Table, Tag, Card, Row, Col, Statistic, Button } from 'antd'
|
|||||||
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
|
|
||||||
interface Task {
|
interface Task {
|
||||||
@@ -146,9 +147,9 @@ function Tasks() {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="table-scroll-region">
|
<TableScrollRegion>
|
||||||
<Table columns={columns} dataSource={tasks} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 'max-content', y: 'calc(100% - 360px)' }} tableLayout="fixed" />
|
<Table columns={columns} dataSource={tasks} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 'max-content', y: 'calc(100% - 360px)' }} tableLayout="fixed" />
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
</Card>
|
</Card>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from '
|
|||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number
|
id: number
|
||||||
@@ -144,7 +145,7 @@ function Users() {
|
|||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加用户</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加用户</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-shell__body">
|
<div className="page-shell__body">
|
||||||
<div ref={tableRegionRef} className="table-scroll-region data-source-table-region users-table-region" style={{ height: '100%' }}>
|
<TableScrollRegion ref={tableRegionRef} className="data-source-table-region users-table-region" style={{ height: '100%' }}>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={users}
|
dataSource={users}
|
||||||
@@ -153,7 +154,7 @@ function Users() {
|
|||||||
scroll={{ x: 'max-content', y: tableHeight }}
|
scroll={{ x: 'max-content', y: tableHeight }}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableScrollRegion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "planet"
|
name = "planet"
|
||||||
version = "0.27.4"
|
version = "0.27.5"
|
||||||
description = "智能星球计划 - 态势感知系统"
|
description = "智能星球计划 - 态势感知系统"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
Reference in New Issue
Block a user