diff --git a/VERSION b/VERSION
index 9f222923..69bf493e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.27.4
+0.27.5
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 09e66bc3..4e541631 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -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
### 🔧 Improvements
diff --git a/docs/ue5_mvp_fused_plan.md b/docs/ue5_mvp_fused_plan.md
new file mode 100644
index 00000000..ac98d434
--- /dev/null
+++ b/docs/ue5_mvp_fused_plan.md
@@ -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。**
diff --git a/docs/version-history.md b/docs/version-history.md
index 4cf398d0..0c613094 100644
--- a/docs/version-history.md
+++ b/docs/version-history.md
@@ -16,12 +16,13 @@
## Current Version
- `main` 当前主线历史推导到:`0.16.5`
-- `dev` 当前开发分支历史推导到:`0.27.4`
+- `dev` 当前开发分支历史推导到:`0.27.5`
## Timeline
| Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- |
+| `0.27.5` | bugfix | `dev` | `pending` | 统一控制台自定义滚动条,修复 alerts/BGP 响应式滚动与采集进度完成态显示 |
| `0.27.4` | improvement | `dev` | — | info-card 懒加载动态挂载,页面初始不再有隐藏节点 |
| `0.27.3` | improvement | `dev` | — | TV panel 折叠方向稳定、视频跳动修复、图例折叠按钮修复、搜索图标调整 |
| `0.27.2` | improvement | `dev` | — | 修复 brand copy 宽度问题,提取 --brand-copy-width CSS 变量 |
diff --git a/frontend/package.json b/frontend/package.json
index a7cffa27..06b6446b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
- "version": "0.27.4",
+ "version": "0.27.5",
"private": true,
"packageManager": "bun@1",
"dependencies": {
diff --git a/frontend/src/components/Scrollbar/Scrollbar.tsx b/frontend/src/components/Scrollbar/Scrollbar.tsx
index 8a80ffba..81ea9fd3 100644
--- a/frontend/src/components/Scrollbar/Scrollbar.tsx
+++ b/frontend/src/components/Scrollbar/Scrollbar.tsx
@@ -265,6 +265,7 @@ function Scrollbar({
{scrollbar.y.visible ? (
(function TableScrollRegion(
+ {
+ children,
+ className = '',
+ style,
+ targetSelector = '.ant-table-body',
+ },
+ ref,
+) {
+ const containerRef = useRef
(null)
+
+ useImperativeHandle(ref, () => containerRef.current as HTMLDivElement, [])
+
+ return (
+
+ {children}
+
+
+ )
+})
+
+export default TableScrollRegion
diff --git a/frontend/src/index.css b/frontend/src/index.css
index c706dd9a..4d1ef31d 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -140,7 +140,8 @@ body {
opacity: 0;
cursor: pointer;
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 {
@@ -173,6 +174,7 @@ body {
}
.scrollbar:hover .scrollbar__track--visible .scrollbar__thumb,
+.scrollbar:focus-within .scrollbar__track--visible .scrollbar__thumb,
.scrollbar__track--visible .scrollbar__thumb,
.scrollbar__track--dragging .scrollbar__thumb {
opacity: 1;
@@ -182,12 +184,15 @@ body {
pointer-events: auto;
}
-.scrollbar__thumb:hover {
- background: rgba(216, 226, 240, 0.68);
+.scrollbar__thumb:hover,
+.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 {
- 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 {
@@ -398,9 +403,11 @@ body {
.playground-card__scroll {
height: 100%;
+}
+
+.playground-card__scroll .scrollbar__viewport {
+ height: 100%;
overflow: auto;
- scrollbar-width: thin;
- scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
}
.playground-card__scroll::-webkit-scrollbar,
@@ -1251,25 +1258,21 @@ body {
.playground-result-modal__content {
flex: 1 1 auto;
min-height: 0;
- overflow: auto;
padding-right: 6px;
- scrollbar-width: thin;
- scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
}
-.playground-result-modal__content::-webkit-scrollbar {
- width: 8px;
+.playground-result-modal__content .scrollbar__viewport {
+ height: 100%;
+ overflow: auto;
}
-.playground-result-modal__content::-webkit-scrollbar-thumb {
- background: rgba(148, 163, 184, 0.82);
- border-radius: 999px;
- border: 2px solid transparent;
- background-clip: padding-box;
+.playground-result__blocks-scroll .scrollbar__viewport {
+ height: 100%;
+ overflow: auto;
}
-.playground-result-modal__content::-webkit-scrollbar-track {
- background: transparent;
+.playground-result__blocks-scroll.scrollbar {
+ min-height: 0;
}
@media (max-width: 1200px) {
@@ -1583,14 +1586,14 @@ body {
padding: 10px 12px !important;
}
-.data-source-table-region .ant-table-body,
-.data-source-table-region .ant-table-content {
+.table-scroll-region .ant-table-body,
+.table-scroll-region .ant-table-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
-.data-source-table-region .ant-table-body::-webkit-scrollbar,
-.data-source-table-region .ant-table-content::-webkit-scrollbar {
+.table-scroll-region .ant-table-body::-webkit-scrollbar,
+.table-scroll-region .ant-table-content::-webkit-scrollbar {
width: 0;
height: 0;
}
@@ -1757,7 +1760,10 @@ body {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
- overflow: auto;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
@@ -1836,10 +1842,11 @@ body {
display: grid;
gap: 14px;
max-height: calc(100vh - 180px);
- overflow: auto;
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 {
@@ -1857,6 +1864,10 @@ body {
gap: 12px;
}
+.alerts-brief-drawer .scrollbar__viewport {
+ overflow: auto;
+}
+
.alerts-brief-drawer__loading {
min-height: 160px;
display: flex;
@@ -1887,8 +1898,17 @@ body {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
+ width: 100%;
display: flex;
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,
@@ -2128,29 +2148,71 @@ body {
line-height: 1;
}
-.bgp-page__summary-grid--compact {
- flex-wrap: nowrap !important;
+.bgp-page__summary-scroll.scrollbar,
+.alerts-summary-scroll.scrollbar {
+ overflow: hidden;
+}
+
+.bgp-page__summary-scroll .scrollbar__viewport,
+.alerts-summary-scroll .scrollbar__viewport {
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 4px;
- scrollbar-width: thin;
- scrollbar-color: rgba(148, 163, 184, 0.82) transparent;
}
-.bgp-page__summary-grid--compact::-webkit-scrollbar {
- width: 8px;
- height: 8px;
+.bgp-page__summary-grid,
+.alerts-summary-grid {
+ display: flex;
+ flex-wrap: nowrap !important;
+ min-width: max-content;
}
-.bgp-page__summary-grid--compact::-webkit-scrollbar-thumb {
- background: rgba(148, 163, 184, 0.82);
- border-radius: 999px;
- border: 2px solid transparent;
- background-clip: padding-box;
+.bgp-page__summary-cell,
+.alerts-summary-cell {
+ flex: 0 0 auto;
}
-.bgp-page__summary-grid--compact::-webkit-scrollbar-track {
- background: transparent;
+.situational-alerts-page__summary-grid {
+ 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,
@@ -2408,25 +2470,21 @@ body {
.settings-panel-scroll {
flex: 1 1 auto;
min-height: 0;
+ padding-right: 6px;
+}
+
+.settings-panel-scroll.scrollbar {
+ overflow: hidden;
+}
+
+.settings-panel-scroll .scrollbar__viewport {
overflow-y: auto;
overflow-x: hidden;
- padding-right: 6px;
scrollbar-gutter: stable;
}
-.settings-panel-scroll::-webkit-scrollbar {
- width: 10px;
-}
-
-.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-panel-scroll .scrollbar__viewport > * {
+ min-width: 0;
}
.settings-pane .data-source-table-region .ant-table-container {
@@ -3273,7 +3331,6 @@ body {
.dashboard-restart-log {
max-height: 180px;
- overflow-y: auto;
padding: 10px 12px;
border-radius: 12px;
background: #0f172a;
@@ -3281,16 +3338,11 @@ body {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
line-height: 1.6;
- scrollbar-width: thin;
}
-.dashboard-restart-log::-webkit-scrollbar {
- width: 8px;
-}
-
-.dashboard-restart-log::-webkit-scrollbar-thumb {
- border-radius: 999px;
- background: rgba(148, 163, 184, 0.55);
+.dashboard-restart-log .scrollbar__viewport {
+ overflow-y: auto;
+ max-height: 180px;
}
@media (max-width: 768px) {
diff --git a/frontend/src/pages/Alerts/BGPAlerts.tsx b/frontend/src/pages/Alerts/BGPAlerts.tsx
index adb0ab88..bbc164a1 100644
--- a/frontend/src/pages/Alerts/BGPAlerts.tsx
+++ b/frontend/src/pages/Alerts/BGPAlerts.tsx
@@ -5,10 +5,8 @@ import {
Alert,
Button,
Card,
- Col,
Descriptions,
Modal,
- Row,
Space,
Spin,
Statistic,
@@ -21,6 +19,8 @@ import {
} from 'antd'
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 { getSituationalAwarenessGateway } from '../../services/situational-awareness'
import { formatDateTimeZhCN } from '../../utils/datetime'
@@ -171,20 +171,22 @@ export function BGPAlertsPanel() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
columns={incidentColumns}
dataSource={incidents}
@@ -204,14 +206,14 @@ export function BGPAlertsPanel() {
scroll={{ x: 1200, y: 480 }}
tableLayout="fixed"
/>
-
+
),
},
{
key: 'anomalies',
label: 'BGP 异常',
children: (
-
+
columns={anomalyColumns}
dataSource={anomalies}
@@ -221,7 +223,7 @@ export function BGPAlertsPanel() {
scroll={{ x: 1100, y: 480 }}
tableLayout="fixed"
/>
-
+
),
},
]}
@@ -244,14 +246,14 @@ export function BGPAlertsPanel() {
) : brief ? (
-
+
{brief.provider || '-'}
{brief.model || '-'}
{formatDateTimeZhCN(brief.generated_at)}
{brief.content_markdown}
-
+
) : (
当前没有可查看的 BGP 简报。
)}
diff --git a/frontend/src/pages/Alerts/SituationalAlerts.tsx b/frontend/src/pages/Alerts/SituationalAlerts.tsx
index 73299ce2..43b64aeb 100644
--- a/frontend/src/pages/Alerts/SituationalAlerts.tsx
+++ b/frontend/src/pages/Alerts/SituationalAlerts.tsx
@@ -5,10 +5,8 @@ import {
Alert,
Button,
Card,
- Col,
Descriptions,
Drawer,
- Row,
Space,
Spin,
Statistic,
@@ -18,6 +16,7 @@ import {
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
+import Scrollbar from '../../components/Scrollbar/Scrollbar'
import type { BGPSummarySnapshot } from '../../services/situational-awareness'
import {
getSituationalAwarenessGateway,
@@ -119,42 +118,46 @@ export function SituationalAlertsPanel() {
message="态势告警不是单一模块列表,而是把系统告警与 BGP 风险综合成一份值班研判入口。"
/>
-
-
- } />
-
-
-
-
-
- } />
-
-
-
-
-
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+
-
-
-
-
- {String(systemStats?.critical ?? '-')}
- {String(systemStats?.warning ?? '-')}
- {String(systemStats?.info ?? '-')}
-
-
-
-
-
-
- {String(bgpSummary?.incidentSummary?.by_status?.active ?? '-')}
- {String(bgpSummary?.incidentSummary?.by_severity?.critical ?? '-')}
- {String(bgpSummary?.collectorSummary?.active_collectors ?? '-')}
- {String(bgpSummary?.collectorSummary?.recent_24h_events ?? '-')}
-
-
-
-
+
+
+
+
+
+ {String(systemStats?.critical ?? '-')}
+ {String(systemStats?.warning ?? '-')}
+ {String(systemStats?.info ?? '-')}
+
+
+
+
+
+
+ {String(bgpSummary?.incidentSummary?.by_status?.active ?? '-')}
+ {String(bgpSummary?.incidentSummary?.by_severity?.critical ?? '-')}
+ {String(bgpSummary?.collectorSummary?.active_collectors ?? '-')}
+ {String(bgpSummary?.collectorSummary?.recent_24h_events ?? '-')}
+
+
+
+
+
setBriefOpen(false)} open={briefOpen}>
diff --git a/frontend/src/pages/Alerts/SystemAlerts.tsx b/frontend/src/pages/Alerts/SystemAlerts.tsx
index 3be8b54d..977fc672 100644
--- a/frontend/src/pages/Alerts/SystemAlerts.tsx
+++ b/frontend/src/pages/Alerts/SystemAlerts.tsx
@@ -5,11 +5,9 @@ import {
Alert,
Button,
Card,
- Col,
Descriptions,
Drawer,
Modal,
- Row,
Space,
Spin,
Statistic,
@@ -22,6 +20,8 @@ import {
import axios from 'axios'
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 { formatDateTimeZhCN } from '../../utils/datetime'
@@ -216,20 +216,22 @@ export function SystemAlertsPanel() {
-
-
- } />
-
-
- } />
-
-
- } />
-
-
+
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+
-
+
columns={columns}
dataSource={alerts}
@@ -239,7 +241,7 @@ export function SystemAlertsPanel() {
scroll={{ x: 1100, y: 480 }}
tableLayout="fixed"
/>
-
+
@@ -260,7 +262,7 @@ export function SystemAlertsPanel() {
setBriefOpen(false)} open={briefOpen}>
-
+
{briefLoading ? (
@@ -293,7 +295,7 @@ export function SystemAlertsPanel() {
) : null}
-
+
diff --git a/frontend/src/pages/BGP/BGP.tsx b/frontend/src/pages/BGP/BGP.tsx
index e16aa54c..9f0b7ec3 100644
--- a/frontend/src/pages/BGP/BGP.tsx
+++ b/frontend/src/pages/BGP/BGP.tsx
@@ -4,10 +4,8 @@ import {
Alert,
Button,
Card,
- Col,
Descriptions,
Modal,
- Row,
Select,
Space,
Spin,
@@ -22,6 +20,8 @@ import {
} from 'antd'
import AppLayout from '../../components/AppLayout/AppLayout'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
+import Scrollbar from '../../components/Scrollbar/Scrollbar'
+import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN } from '../../utils/datetime'
import {
type BGPAnomaly,
@@ -708,37 +708,36 @@ function BGP() {
/>
-
- {summaryItems.map((item) => (
-
-
-
{item.label}
-
+
+
+ {summaryItems.map((item) => (
+
-
- ))}
-
+ ))}
+
+
-
+
@@ -755,7 +754,7 @@ function BGP() {
destroyOnHidden
>
{brief ? (
-
+
{(brief.facts.length > 0 || Object.keys(brief.context || {}).length > 0) ? (
{brief.facts.length > 0 ? (
@@ -785,7 +784,7 @@ function BGP() {
) : null}
-
+
) : (
当前没有可查看的简报内容。
)}
diff --git a/frontend/src/pages/Dashboard/Dashboard.tsx b/frontend/src/pages/Dashboard/Dashboard.tsx
index 4f70b910..68774a24 100644
--- a/frontend/src/pages/Dashboard/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard/Dashboard.tsx
@@ -14,6 +14,7 @@ import { Link } from 'react-router-dom'
import axios from 'axios'
import { useAuthStore } from '../../stores/auth'
import AppLayout from '../../components/AppLayout/AppLayout'
+import Scrollbar from '../../components/Scrollbar/Scrollbar'
import { useWebSocket } from '../../hooks/useWebSocket'
import { formatDateTimeZhCN } from '../../utils/datetime'
@@ -532,11 +533,11 @@ function Dashboard() {
终端输出
-
+
{restartLogs.length > 0 ? restartLogs.map((line, index) => (
{line}
)) : 等待操作
}
-
+
diff --git a/frontend/src/pages/DataList/DataList.tsx b/frontend/src/pages/DataList/DataList.tsx
index abedf157..7f990d6f 100644
--- a/frontend/src/pages/DataList/DataList.tsx
+++ b/frontend/src/pages/DataList/DataList.tsx
@@ -12,6 +12,7 @@ import {
} from '@ant-design/icons'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
+import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN, formatDateZhCN, parseBackendDate } from '../../utils/datetime'
const { Title, Text } = Typography
@@ -939,7 +940,7 @@ function DataList() {
/>
-
+
`共 ${count} 条`,
}}
/>
-
+
diff --git a/frontend/src/pages/DataSources/DataSources.tsx b/frontend/src/pages/DataSources/DataSources.tsx
index 6c07933b..0cf693ce 100644
--- a/frontend/src/pages/DataSources/DataSources.tsx
+++ b/frontend/src/pages/DataSources/DataSources.tsx
@@ -116,18 +116,39 @@ function finalizeBulkProgressBatch(batch: BulkProgressBatch | null): BulkProgres
if (!batch || batch.sourceIds.length === 0) {
return null
}
+ return batch
+}
- const hasRunningItem = batch.sourceIds.some((sourceId) => batch.items[sourceId]?.is_running)
- if (hasRunningItem) {
- return batch
+function resolveTerminalBatchItem(
+ sourceId: number,
+ batch: BulkProgressBatch,
+ builtInSources: BuiltInDataSource[],
+ taskProgress: Record,
+): 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 = batch.items[sourceId]?.status
- return Boolean(status && status !== 'running')
- })
+ const status = trackedTask?.status ?? source?.last_status ?? currentItem?.status ?? null
+ if (!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 {
@@ -436,6 +457,41 @@ function DataSources() {
return () => clearInterval(interval)
}, [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 force = options?.force ?? false
const res = await axios.post(`/api/v1/datasources/${id}/trigger`, null, {
diff --git a/frontend/src/pages/Playground/Playground.tsx b/frontend/src/pages/Playground/Playground.tsx
index 60198463..a01cb430 100644
--- a/frontend/src/pages/Playground/Playground.tsx
+++ b/frontend/src/pages/Playground/Playground.tsx
@@ -22,6 +22,8 @@ import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
+import Scrollbar from '../../components/Scrollbar/Scrollbar'
+import ScrollbarOverlay from '../../components/Scrollbar/ScrollbarOverlay'
import { useAuthStore } from '../../stores/auth'
const { Title, Text, Paragraph } = Typography
@@ -239,6 +241,7 @@ function Playground() {
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const pollTimerRef = useRef(null)
const messagesContainerRef = useRef(null)
+ const messagesShellRef = useRef(null)
const forceScrollToBottomRef = useRef(true)
const selectedPreset = useMemo(
@@ -575,7 +578,7 @@ function Playground() {
/>
)}
>
-
+
{providerStatus ? (
@@ -617,7 +620,7 @@ function Playground() {
)}
-
+
)
@@ -699,7 +702,7 @@ function Playground() {
)}
>
-
+
{messages.map((entry) => (
@@ -760,8 +763,9 @@ function Playground() {
-
-
+
+
+
) : (entry.role !== 'assistant' || entry.status === 'answering' || entry.status === 'done' || entry.status === 'stopped' || entry.status === 'error') && entry.markdown ? (
@@ -1000,42 +1004,42 @@ function Playground() {
/>
-
+
{analysis.text_blocks.length ? (
文本块
-
+
{analysis.text_blocks.map((block, index) => (
{block}
))}
-
+
) : null}
{analysis.thinking_blocks.length ? (
Thinking Blocks
-
+
{analysis.thinking_blocks.map((block, index) => (
{block}
))}
-
+
) : null}
Raw Response
-
+
{JSON.stringify(analysis.raw_response, null, 2)}
-
+
-
+
) : null}
diff --git a/frontend/src/pages/Settings/Settings.tsx b/frontend/src/pages/Settings/Settings.tsx
index d684dff0..7b29eb83 100644
--- a/frontend/src/pages/Settings/Settings.tsx
+++ b/frontend/src/pages/Settings/Settings.tsx
@@ -15,6 +15,8 @@ import {
} from 'antd'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
+import Scrollbar from '../../components/Scrollbar/Scrollbar'
+import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN } from '../../utils/datetime'
const { Title, Text } = Typography
@@ -91,7 +93,7 @@ function SettingsPanel({
return (
- {children}
+ {children}
)
@@ -642,7 +644,7 @@ function Settings() {
-
+
@@ -668,7 +670,7 @@ function Settings() {
loading={loading}
styles={{ body: { padding: 0 } }}
>
-
+
),
diff --git a/frontend/src/pages/Tasks/Tasks.tsx b/frontend/src/pages/Tasks/Tasks.tsx
index dbb1f183..f7af6279 100644
--- a/frontend/src/pages/Tasks/Tasks.tsx
+++ b/frontend/src/pages/Tasks/Tasks.tsx
@@ -3,6 +3,7 @@ import { Table, Tag, Card, Row, Col, Statistic, Button } from 'antd'
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
import AppLayout from '../../components/AppLayout/AppLayout'
+import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN } from '../../utils/datetime'
interface Task {
@@ -146,9 +147,9 @@ function Tasks() {
}
>
-
+
)
diff --git a/frontend/src/pages/Users/Users.tsx b/frontend/src/pages/Users/Users.tsx
index 71d35e7d..01c88d85 100644
--- a/frontend/src/pages/Users/Users.tsx
+++ b/frontend/src/pages/Users/Users.tsx
@@ -3,6 +3,7 @@ import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from '
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
+import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
interface User {
id: number
@@ -144,7 +145,7 @@ function Users() {
} onClick={handleAdd}>添加用户