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) => ( +
+
+
{item.label}
+ +
- - ))} - + ))} +
+
-
+ -
+
@@ -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() {
-
+
- +