feat(frontend): remodel status monitor panel and show tags in global search
refactor workspace status monitoring UI into a dense responsive dark panel style, including unified header, resource cards, and trend chart shells. enhance global connection quick search cards by rendering connection tag chips from tags store for easier host disambiguation. update helloagents changelog, archive proposals/tasks, and module index to record both completed frontend improvements.
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **[frontend]**: 将工作区状态监控重构为更接近服务器监控小屏的深色响应式面板,统一头部信息条、资源监控条、内存/网络/磁盘卡片及 CPU/网络趋势图风格 — by yinjianm
|
||||
- 方案: [202604152109_status-monitor-responsive-remodel](archive/2026-04/202604152109_status-monitor-responsive-remodel/)
|
||||
|
||||
- **[frontend]**: 为全局服务器检索结果卡片补充服务器标签显示,便于在 `Ctrl+Shift+F` 快速检索时区分同名或近似主机 - by yinjianm
|
||||
- 方案: [202604152110_workspace-global-search-show-connection-tags](archive/2026-04/202604152110_workspace-global-search-show-connection-tags/)
|
||||
|
||||
- **[frontend]**: 修复持续日志输出时切换终端后的 viewport 恢复偏移问题,改为按距底部偏移恢复滚动位置,避免重新激活后无法继续向下滚到最底部 — by yinjianm
|
||||
- 方案: [202604120705_terminal-scroll-viewport-restore-fix](archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/)
|
||||
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# 变更提案: status-monitor-responsive-remodel
|
||||
|
||||
## 元信息
|
||||
```yaml
|
||||
类型: 重构/优化
|
||||
方案类型: implementation
|
||||
优先级: P1
|
||||
状态: 已确认
|
||||
创建: 2026-04-15
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求
|
||||
|
||||
### 背景
|
||||
当前 `StatusMonitor.vue` 已经具备基础监控能力,但信息组织仍然偏表单式和松散卡片式,与用户给出的高密度监控参考图不一致。用户要求将“服务器状态”改造成统一的深色监控面板风格,同时明确指出不能直接照搬截图,因为现有组件结构、子组件边界和可用数据字段都与截图不同。
|
||||
|
||||
### 目标
|
||||
- 在不改动后端数据结构的前提下,将 `StatusMonitor.vue` 重组为更接近参考图的高密度状态监控布局
|
||||
- 同步调整 `StatusCharts.vue`,使底部趋势图与主监控区风格统一,避免视觉断层
|
||||
- 保持侧栏窄宽度、常规工作区宽度和移动端下都能稳定展示,保证响应式体验
|
||||
|
||||
### 约束条件
|
||||
```yaml
|
||||
时间约束: 当前回合内完成设计、实现与验证
|
||||
性能约束: 保持现有实时刷新逻辑,不引入额外轮询或高频动画负担
|
||||
兼容性约束: 不修改 ServerStatus 数据模型,不破坏现有会话切换、历史趋势和复制 IP 等行为
|
||||
业务约束: 仅使用现有 CPU/内存/Swap/网络/磁盘字段做“视觉映射”,不伪造参考图中不存在的数据组件
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
- [ ] `StatusMonitor.vue` 输出统一的深色监控面板视觉,系统信息、CPU、内存、网络、磁盘分区清晰且整体节奏接近参考图
|
||||
- [ ] `StatusCharts.vue` 与主监控区采用同一风格语言,图表容器、标题、边框、配色统一
|
||||
- [ ] 在窄侧栏和宽容器下布局都可读,关键模块不会溢出或只在单一宽度下可用
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案
|
||||
|
||||
### 技术方案
|
||||
保留当前 `ServerStatus` 数据模型和 `StatusCharts` 的历史数据来源,仅重构前端展示层。
|
||||
|
||||
具体做法:
|
||||
- 重写 `StatusMonitor.vue` 模板结构,将零散状态行重排为“头部信息条 + 资源总览区 + 分块卡片 + 趋势区”的紧凑监控布局
|
||||
- 复用现有计算属性与格式化逻辑,并补充少量只读映射计算,使 CPU/Swap、内存统计、网络吞吐、磁盘信息可以更贴近参考图表达
|
||||
- 改造 `StatusCharts.vue` 的图表外壳与配色,使其与主卡片共享同一套面板语言和暗色调
|
||||
- 通过容器断点与媒体断点双层控制,针对侧栏窄宽度优先纵向堆叠,宽屏再切为多列布局
|
||||
|
||||
### 影响范围
|
||||
```yaml
|
||||
涉及模块:
|
||||
- packages/frontend/src/components/StatusMonitor.vue: 主监控面板结构与样式重构
|
||||
- packages/frontend/src/components/StatusCharts.vue: 趋势图外壳与主题风格统一
|
||||
- .helloagents/CHANGELOG.md: 记录本次实现型变更
|
||||
预计变更文件: 3
|
||||
```
|
||||
|
||||
### 风险评估
|
||||
| 风险 | 等级 | 应对 |
|
||||
|------|------|------|
|
||||
| 参考图与现有组件结构不一致,易出现“强行复刻”后语义不通 | 中 | 按现有字段重组视觉模块,只在展示层做映射,不硬拼不存在的数据块 |
|
||||
| 侧栏宽度较窄,复杂卡片容易挤压变形 | 中 | 使用 container query + media query 双层适配,优先保证窄宽度可读性 |
|
||||
| 图表风格与主区域脱节 | 低 | 同步调整 `StatusCharts.vue` 的面板壳层、标题层级和配色体系 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术设计(可选)
|
||||
|
||||
> 本次无后端接口和数据模型变更,仅描述前端组件关系调整。
|
||||
|
||||
### 架构设计
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Session Store / statusMonitorManager] --> B[StatusMonitor.vue]
|
||||
B --> C[头部信息条]
|
||||
B --> D[资源总览卡片]
|
||||
B --> E[磁盘与网络明细]
|
||||
B --> F[StatusCharts.vue]
|
||||
```
|
||||
|
||||
### API设计
|
||||
N/A
|
||||
|
||||
### 数据模型
|
||||
N/A
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心场景
|
||||
|
||||
> 执行完成后同步到对应模块文档
|
||||
|
||||
### 场景: 窄侧栏中的服务器状态监控
|
||||
**模块**: `packages/frontend/src/components/StatusMonitor.vue`
|
||||
**条件**: 用户已连接活动会话,状态监控面板显示在侧栏或窄容器中
|
||||
**行为**: 组件将系统信息、资源指标、磁盘和网络信息压缩为高密度纵向监控布局,并将趋势图保持为统一风格的可读模块
|
||||
**结果**: 用户在窄宽度下也能快速看到核心监控信息,界面风格与参考图接近且保持结构自洽
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术决策
|
||||
|
||||
> 本方案涉及的技术决策,归档后成为决策的唯一完整记录
|
||||
|
||||
### status-monitor-responsive-remodel#D001: 基于现有组件数据结构做视觉重构,而非截图式逐像素复刻
|
||||
**日期**: 2026-04-15
|
||||
**状态**: ✅采纳
|
||||
**背景**: 用户给出的参考图和仓库内现有状态监控组件并不是同一套数据组织方式。若按截图硬做,会引入伪数据展示和组件结构错位。
|
||||
**选项分析**:
|
||||
| 选项 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| A: 直接按截图复刻 | 视觉最接近参考图 | 容易出现字段不匹配、组件语义错位、维护成本高 |
|
||||
| B: 基于现有字段和组件边界做同风格重构 | 结构真实、可维护、能保留现有行为 | 与截图不会 1:1 完全一致 |
|
||||
**决策**: 选择方案 B
|
||||
**理由**: 用户已经明确提醒要注意组件情况,因此应优先尊重现有组件结构和真实数据边界,在此基础上做到风格、密度和节奏接近参考图。
|
||||
**影响**: 影响 `StatusMonitor.vue` 与 `StatusCharts.vue` 的结构组织、样式语言和响应式策略
|
||||
|
||||
---
|
||||
|
||||
## 6. 成果设计
|
||||
|
||||
> 含视觉产出的任务由 DESIGN Phase2 填充。非视觉任务整节标注"N/A"。
|
||||
|
||||
### 设计方向
|
||||
- **美学基调**: 工业监控台风格的深色数据面板,强调高密度、低装饰噪音、荧光状态色和类似运维控制面板的秩序感
|
||||
- **记忆点**: 头部信息条与纵向资源块形成“窄屏也像真实服务器监控小屏”的强监控感
|
||||
- **参考**: 用户提供的服务器状态截图;但实现以当前组件结构和真实字段为准
|
||||
|
||||
### 视觉要素
|
||||
- **配色**: 以接近 `#14181d` 的炭黑背景为基底,辅以薄荷绿状态线、青蓝 CPU/网络高亮、琥珀磁盘标签和红色告警环形占比
|
||||
- **字体**: 延续项目现有字体体系,不额外引入远程字体;通过更紧凑的字号层级、等宽数字和大写微标签来形成监控台气质
|
||||
- **布局**: 顶部系统信息先行,资源块以纵向密集分段排列;在更宽容器中让次级统计变为网格,窄容器中退化为单列
|
||||
- **动效**: 保留进度和图表的实时变化动势,避免夸张过渡;重点使用轻量 hover 和状态高亮
|
||||
- **氛围**: 使用深色渐变、细边框、内阴影、弱发光线条和压低透明度的分隔来塑造监控终端氛围
|
||||
|
||||
### 技术约束
|
||||
- **可访问性**: 保持文本和背景对比度;保留语义化标题与可点击 IP 的交互反馈
|
||||
- **响应式**: 同时使用 container query 与 `@media` 断点;窄容器单列堆叠,较宽容器切回分栏
|
||||
@@ -0,0 +1,56 @@
|
||||
# 任务清单: status-monitor-responsive-remodel
|
||||
|
||||
> **@status:** completed | 2026-04-15 21:32
|
||||
|
||||
```yaml
|
||||
@feature: status-monitor-responsive-remodel
|
||||
@created: 2026-04-15
|
||||
@status: completed
|
||||
@mode: R2
|
||||
```
|
||||
|
||||
## 进度概览
|
||||
|
||||
| 完成 | 失败 | 跳过 | 总数 |
|
||||
|------|------|------|------|
|
||||
| 5 | 0 | 0 | 5 |
|
||||
|
||||
---
|
||||
|
||||
## 任务列表
|
||||
|
||||
### 1. 方案与结构确认
|
||||
|
||||
- [√] 1.1 完成现有状态监控组件、子组件与数据字段的对齐分析,明确哪些视觉块可由现有字段支撑 | depends_on: []
|
||||
|
||||
### 2. 主监控区重构
|
||||
|
||||
- [√] 2.1 在 `packages/frontend/src/components/StatusMonitor.vue` 中重构头部信息区、CPU/内存/网络/磁盘模块布局与样式 | depends_on: [1.1]
|
||||
- [√] 2.2 在 `packages/frontend/src/components/StatusMonitor.vue` 中补齐窄侧栏与较宽容器下的响应式适配 | depends_on: [2.1]
|
||||
|
||||
### 3. 趋势图风格统一
|
||||
|
||||
- [√] 3.1 在 `packages/frontend/src/components/StatusCharts.vue` 中统一图表容器、标题层级与暗色监控风格 | depends_on: [2.1]
|
||||
|
||||
### 4. 验证与记录
|
||||
|
||||
- [√] 4.1 执行前端构建验证并同步记录方案包与 CHANGELOG | depends_on: [2.2, 3.1]
|
||||
|
||||
---
|
||||
|
||||
## 执行日志
|
||||
|
||||
| 时间 | 任务 | 状态 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 2026-04-15 21:09 | 1.1 | completed | 已确认采用“按现有组件数据结构做同风格重构”的实现路线 |
|
||||
| 2026-04-15 21:22 | 2.1 / 2.2 | completed | `StatusMonitor.vue` 已重排为头部信息条 + 资源监控条 + 内存/网络/磁盘卡片布局,并补齐容器级响应式规则 |
|
||||
| 2026-04-15 21:24 | 3.1 | completed | `StatusCharts.vue` 已统一为同风格深色图表面板 |
|
||||
| 2026-04-15 21:25 | 4.1 | completed | `npm run build` 通过;保留现有 Vite 动态导入和大 chunk 警告,未新增本次改动导致的错误 |
|
||||
|
||||
---
|
||||
|
||||
## 执行备注
|
||||
|
||||
- 用户明确要求注意现有组件边界,禁止对参考图做脱离数据结构的硬复刻
|
||||
- 本次实现默认保留现有 `ServerStatus` 字段模型、历史趋势来源和会话相关行为
|
||||
- 构建阶段存在仓库既有的动态导入 chunk 警告,但本次改动未引入新的阻断性构建错误
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"status":"completed","completed":4,"failed":0,"pending":0,"total":4,"done":4,"percent":100,"current":"Completed - global connection quick search tags visible in result cards","updated_at":"2026-04-15 21:19:00"}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# 变更提案: workspace-global-search-show-connection-tags
|
||||
|
||||
## 元信息
|
||||
```yaml
|
||||
类型: 优化
|
||||
方案类型: implementation
|
||||
优先级: P2
|
||||
状态: 草稿
|
||||
创建: 2026-04-15
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求
|
||||
|
||||
### 背景
|
||||
当前“全局服务器检索”弹层由 [`packages/frontend/src/components/GlobalConnectionQuickSearch.vue`](../../../../packages/frontend/src/components/GlobalConnectionQuickSearch.vue) 渲染,结果项只展示服务器名称、类型、地址和用户名。仓库中的连接对象已经具备 `tag_ids`,标签列表也有现成的 `tags.store.ts` 缓存与拉取逻辑,但该弹层没有把服务器标签显示出来,导致同名或相似主机难以快速区分。
|
||||
|
||||
### 目标
|
||||
- 在全局服务器检索结果项中显示服务器标签。
|
||||
- 保持现有快捷检索、键盘导航和点击连接行为不变。
|
||||
- 仅做前端局部增强,不引入后端、接口或数据模型改动。
|
||||
|
||||
### 约束条件
|
||||
```yaml
|
||||
时间约束: 本轮内完成组件增强与基础验证
|
||||
性能约束: 结果列表最多展示 8 条,标签映射应在组件内轻量计算
|
||||
兼容性约束: 不改变现有 ConnectionInfo 结构与 searchConnections 返回格式
|
||||
业务约束: 仅增强“全局服务器检索”弹层,不扩展到其他连接列表入口
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
- [ ] 打开“全局服务器检索”后,带标签的服务器结果项能在卡片内看到标签 chips。
|
||||
- [ ] 无标签的服务器结果项仍可正常展示、导航和连接,不因标签缺失报错。
|
||||
- [ ] 保持当前名称、类型、地址、用户名信息布局可读,不出现明显挤压或换行错乱。
|
||||
- [ ] 前端构建或类型检查通过,至少验证本组件改动未引入编译错误。
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案
|
||||
|
||||
### 技术方案
|
||||
在 `GlobalConnectionQuickSearch.vue` 内直接接入 `useTagsStore()`,复用现有标签缓存并在组件挂载时补拉一次标签数据。通过本地 `computed` 建立 `tagId -> tagName` 映射,再为每条连接解析 `tag_ids` 对应的标签名称,在结果卡片的元信息区域下方增加一行轻量标签 chips。实现范围限定在展示层,不改动 `connectionSearch.ts` 的搜索评分逻辑。
|
||||
|
||||
### 影响范围
|
||||
```yaml
|
||||
涉及模块:
|
||||
- frontend: 全局服务器检索弹层结果项增加标签展示
|
||||
- knowledge-base: 新增方案包并记录本次前端优化
|
||||
预计变更文件: 4
|
||||
```
|
||||
|
||||
### 风险评估
|
||||
| 风险 | 等级 | 应对 |
|
||||
|------|------|------|
|
||||
| 标签数据在弹层首次打开时尚未加载完成,结果项短时间内无标签 | 低 | 组件挂载时主动 `fetchTags()`,并允许标签缺失时静默降级 |
|
||||
| 标签过多导致结果卡片高度膨胀或信息拥挤 | 中 | 使用小号 chips、换行展示,并限制在现有卡片信息区内 |
|
||||
| 为该弹层单独接入 tags store 造成多余耦合 | 低 | 仅复用现有 store,不新增 props 链路或接口改造 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术设计(可选)
|
||||
|
||||
> 本次不涉及架构、API 或数据模型变更,N/A。
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心场景
|
||||
|
||||
### 场景: 通过标签区分全局检索结果
|
||||
**模块**: frontend
|
||||
**条件**: 用户打开“全局服务器检索”,列表中存在已绑定标签的连接
|
||||
**行为**: 结果项在名称/类型信息下方额外展示该连接的标签 chips
|
||||
**结果**: 用户可以更快区分环境、分组或用途相近的服务器
|
||||
|
||||
### 场景: 无标签服务器保持兼容
|
||||
**模块**: frontend
|
||||
**条件**: 用户打开“全局服务器检索”,结果中存在未绑定标签的连接
|
||||
**行为**: 结果项继续展示现有信息,不额外报错或阻断操作
|
||||
**结果**: 快捷检索、键盘选择和连接动作保持原样可用
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术决策
|
||||
|
||||
### workspace-global-search-show-connection-tags#D001: 在搜索弹层内部直接复用 tags store
|
||||
**日期**: 2026-04-15
|
||||
**状态**: ✅采纳
|
||||
**背景**: 需要为全局服务器检索显示标签,但当前 `App.vue` 仅向该组件传入 `connections` 和 `isLoading`,没有透传标签数据。
|
||||
**选项分析**:
|
||||
| 选项 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| A: 在 `App.vue` 继续向下传递标签 props | 数据流显式 | 需要扩大父组件耦合面,修改调用链 |
|
||||
| B: 在 `GlobalConnectionQuickSearch.vue` 内直接复用 `tags.store.ts` | 改动最小,可直接复用现有缓存与接口 | 组件对 tags store 有额外依赖 |
|
||||
**决策**: 选择方案 B
|
||||
**理由**: 本次是局部展示增强,优先选择最小改动路径,避免为一个展示字段重构父组件 props。
|
||||
**影响**: frontend
|
||||
|
||||
---
|
||||
|
||||
## 6. 成果设计
|
||||
|
||||
### 设计方向
|
||||
- **美学基调**: 延续现有全局检索弹层的深色命令面板视觉,在结果卡片中加入低对比度、轻量化标签 chips,强化“检索信息补充”而不是“主信息抢占”。
|
||||
- **记忆点**: 搜索结果在紧凑卡片中同时保留连接类型 badge 与标签 chips,形成一眼可辨识的服务器上下文。
|
||||
- **参考**: 复用 [`packages/frontend/src/views/ConnectionsView.vue`](../../../../packages/frontend/src/views/ConnectionsView.vue) 现有标签 pills 风格。
|
||||
|
||||
### 视觉要素
|
||||
- **配色**: 延续 `bg-header`、`border-border`、`text-text-secondary` 的现有系统色,不新增主题色。
|
||||
- **字体**: 继续使用项目现有字体栈,标签文本维持 `text-[11px]` 级别作为辅助信息。
|
||||
- **布局**: 标签位于结果项第二信息层,单独一行换行展示,不挤占标题与连接地址主信息。
|
||||
- **动效**: 复用结果卡片已有 hover/selected 态,不新增额外动效。
|
||||
- **氛围**: 保持紧凑、专业、偏命令面板式检索体验,以信息清晰为先。
|
||||
|
||||
### 技术约束
|
||||
- **可访问性**: 标签文字需保持足够对比度,不影响现有键盘导航焦点可读性。
|
||||
- **响应式**: 结果卡片内标签允许自动换行,避免在窄宽度弹层下溢出。
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# 任务清单: workspace-global-search-show-connection-tags
|
||||
|
||||
> **@status:** completed | 2026-04-15 21:34
|
||||
|
||||
```yaml
|
||||
@feature: workspace-global-search-show-connection-tags
|
||||
@created: 2026-04-15
|
||||
@status: completed
|
||||
@mode: R2
|
||||
```
|
||||
|
||||
## 进度概览
|
||||
|
||||
| 完成 | 失败 | 跳过 | 总数 |
|
||||
|------|------|------|------|
|
||||
| 4 | 0 | 0 | 4 |
|
||||
|
||||
---
|
||||
|
||||
## 任务列表
|
||||
|
||||
### 1. 方案设计
|
||||
|
||||
- [√] 1.1 完成全局服务器检索入口定位,确认实际组件为 `packages/frontend/src/components/GlobalConnectionQuickSearch.vue` | depends_on: []
|
||||
|
||||
### 2. 前端实现
|
||||
|
||||
- [√] 2.1 在 `packages/frontend/src/components/GlobalConnectionQuickSearch.vue` 中接入 `tags.store.ts`,建立标签名称映射并为结果项提供标签数据 | depends_on: [1.1]
|
||||
- [√] 2.2 在 `packages/frontend/src/components/GlobalConnectionQuickSearch.vue` 中为搜索结果卡片增加标签 chips 展示,并保持现有信息布局稳定 | depends_on: [2.1]
|
||||
|
||||
### 3. 验证与知识同步
|
||||
|
||||
- [√] 3.1 执行前端构建或等价校验,确认全局服务器检索标签展示改动未引入编译错误 | depends_on: [2.2]
|
||||
|
||||
---
|
||||
|
||||
## 执行日志
|
||||
|
||||
| 时间 | 任务 | 状态 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 2026-04-15 21:10 | DESIGN | completed | 已确认“全局服务器检索”实际入口为 `GlobalConnectionQuickSearch.vue`,并收敛为单组件展示增强 |
|
||||
| 2026-04-15 21:15 | 2.1-2.2 | completed | 已在 `GlobalConnectionQuickSearch.vue` 内接入 `tags.store.ts` 并为结果卡片新增标签 chips |
|
||||
| 2026-04-15 21:19 | 3.1 | completed | `npm --workspace @nexus-terminal/frontend run build` 被仓库现有 `StatusMonitor.vue` 类型错误阻断;`npm --workspace @nexus-terminal/frontend exec vite build` 通过,确认本次组件改动可正常打包 |
|
||||
|
||||
---
|
||||
|
||||
## 执行备注
|
||||
|
||||
> 本次范围限定为“全局服务器检索”弹层,不扩展到 `WorkspaceConnectionList.vue` 或其他连接列表入口。若后续需要统一所有搜索/列表入口的标签展示,再单独起方案包处理。
|
||||
>
|
||||
> 当前仓库存在与本次改动无关的前端类型问题:`packages/frontend/src/components/StatusMonitor.vue` 缺少若干模板引用属性定义,导致全量 `vue-tsc --noEmit` 失败。
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
| 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 |
|
||||
|--------|------|------|---------|------|------|
|
||||
| 202604152110 | workspace-global-search-show-connection-tags | - | - | - | ✅完成 |
|
||||
| 202604152109 | status-monitor-responsive-remodel | - | - | - | ✅完成 |
|
||||
| 202604122248 | connections-tag-batch-management | implementation | frontend, backend | connections-tag-batch-management#D001 | ✅完成 |
|
||||
| 202604120709 | quickcommands-double-click-tooltip | implementation | frontend | quickcommands-double-click-tooltip#D001 | ✅完成 |
|
||||
| 202604120705 | terminal-scroll-viewport-restore-fix | - | - | - | ✅完成 |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { ConnectionInfo } from '../stores/connections.store';
|
||||
import { useTagsStore } from '../stores/tags.store';
|
||||
import { searchConnections } from '../utils/connectionSearch';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -15,6 +17,8 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const tagsStore = useTagsStore();
|
||||
const { tags } = storeToRefs(tagsStore);
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
const query = ref('');
|
||||
const selectedIndex = ref(0);
|
||||
@@ -35,6 +39,7 @@ watch(results, async (nextResults) => {
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
void tagsStore.fetchTags();
|
||||
await nextTick();
|
||||
inputRef.value?.focus();
|
||||
inputRef.value?.select();
|
||||
@@ -90,6 +95,24 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
||||
};
|
||||
|
||||
const getConnectionLabel = (connection: ConnectionInfo): string => connection.name || connection.host;
|
||||
|
||||
const tagLookup = computed(() => {
|
||||
const map = new Map<number, string>();
|
||||
tags.value.forEach((tag) => {
|
||||
map.set(tag.id, tag.name);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const getConnectionTagNames = (connection: ConnectionInfo): string[] => {
|
||||
if (!connection.tag_ids?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return connection.tag_ids
|
||||
.map((tagId) => tagLookup.value.get(tagId))
|
||||
.filter((tagName): tagName is string => Boolean(tagName));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -166,6 +189,18 @@ const getConnectionLabel = (connection: ConnectionInfo): string => connection.na
|
||||
<span>{{ item.connection.host }}:{{ item.connection.port }}</span>
|
||||
<span>{{ item.connection.username }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="getConnectionTagNames(item.connection).length > 0"
|
||||
class="mt-2 flex flex-wrap gap-1.5"
|
||||
>
|
||||
<span
|
||||
v-for="tagName in getConnectionTagNames(item.connection)"
|
||||
:key="`${item.connection.id}-${tagName}`"
|
||||
class="rounded-full border border-border bg-header px-2 py-0.5 text-[11px] text-text-secondary"
|
||||
>
|
||||
{{ tagName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,46 +1,37 @@
|
||||
<template>
|
||||
<div class="status-charts grid grid-cols-1 gap-4 mt-4">
|
||||
<div class="chart-container bg-header rounded p-3">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5 class="text-sm font-medium text-text-secondary">{{ $t('statusMonitor.cpuUsageTitle') }}</h5>
|
||||
<span class="text-xs text-text-tertiary ml-2">
|
||||
{{ $t('statusMonitor.latestCpuValue', { value: cpuChartData.datasets[0].data[MAX_DATA_POINTS - 1]?.toFixed(1) }) }}
|
||||
</span>
|
||||
<div class="status-charts">
|
||||
<section class="chart-panel">
|
||||
<div class="chart-panel__header">
|
||||
<div>
|
||||
<h5 class="chart-panel__title">{{ $t('statusMonitor.cpuUsageTitle') }}</h5>
|
||||
<p class="chart-panel__subtitle">{{ $t('statusMonitor.latestCpuValue', { value: cpuChartData.datasets[0].data[MAX_DATA_POINTS - 1]?.toFixed(1) }) }}</p>
|
||||
</div>
|
||||
<span class="chart-panel__badge chart-panel__badge--cpu">CPU</span>
|
||||
</div>
|
||||
<div class="chart-wrapper h-40">
|
||||
<div class="chart-wrapper">
|
||||
<Line :data="cpuChartData" :options="percentageChartOptions" :key="cpuChartKey" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 内存使用图表已注释掉 -->
|
||||
<!--
|
||||
<div class="chart-container bg-header rounded p-3">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5 class="text-sm font-medium text-text-secondary">{{ $t('statusMonitor.memoryUsageTitleUnit', { unit: memoryUnitIsGB ? 'GB' : 'MB' }) }}</h5>
|
||||
<span class="text-xs text-text-tertiary ml-2">
|
||||
{{ $t('statusMonitor.latestMemoryValue', { value: memoryChartData.datasets[0].data[MAX_DATA_POINTS - 1]?.toFixed(1), unit: memoryUnitIsGB ? 'GB' : 'MB' }) }}
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<section class="chart-panel">
|
||||
<div class="chart-panel__header">
|
||||
<div>
|
||||
<h5 class="chart-panel__title">{{ $t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitIsMB ? 'MB/s' : 'KB/s' }) }}</h5>
|
||||
<p class="chart-panel__subtitle">
|
||||
{{ $t('statusMonitor.latestNetworkValue', { download: networkChartData.datasets[0].data[MAX_DATA_POINTS - 1]?.toFixed(1), upload: networkChartData.datasets[1].data[MAX_DATA_POINTS - 1]?.toFixed(1), unit: networkRateUnitIsMB ? 'MB/s' : 'KB/s' }) }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="chart-panel__badge chart-panel__badge--network">NET</span>
|
||||
</div>
|
||||
<div class="chart-wrapper h-40">
|
||||
<Line :data="memoryChartData" :options="memoryChartOptions" :key="memoryChartKey" />
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="chart-container bg-header rounded p-3">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5 class="text-sm font-medium text-text-secondary">{{ $t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitIsMB ? 'MB/s' : 'KB/s' }) }}</h5>
|
||||
<span class="text-xs text-text-tertiary ml-2">
|
||||
{{ $t('statusMonitor.latestNetworkValue', { download: networkChartData.datasets[0].data[MAX_DATA_POINTS - 1]?.toFixed(1), upload: networkChartData.datasets[1].data[MAX_DATA_POINTS - 1]?.toFixed(1), unit: networkRateUnitIsMB ? 'MB/s' : 'KB/s' }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="chart-wrapper h-40">
|
||||
<div class="chart-wrapper">
|
||||
<Line :data="networkChartData" :options="networkChartOptions" :key="networkChartKey" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed, type PropType } from 'vue';
|
||||
import { ref, watch, computed, type PropType } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Line } from 'vue-chartjs';
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
@@ -238,12 +229,15 @@ const networkChartData = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const axisTickColor = '#8fa0b3';
|
||||
const axisGridColor = 'rgba(148, 163, 184, 0.12)';
|
||||
|
||||
const baseChartOptions: Omit<ChartOptions<'line'>, 'scales'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: { labels: { color: '#9CA3AF' } },
|
||||
legend: { labels: { color: axisTickColor } },
|
||||
tooltip: { enabled: true, mode: 'index', intersect: false },
|
||||
},
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
@@ -256,11 +250,11 @@ const percentageChartOptions = ref<ChartOptions<'line'>>({ // For CPU
|
||||
beginAtZero: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
ticks: { color: '#9CA3AF', callback: value => `${value}%` },
|
||||
grid: { color: 'rgba(156, 163, 175, 0.1)' },
|
||||
ticks: { color: axisTickColor, callback: value => `${value}%` },
|
||||
grid: { color: axisGridColor },
|
||||
},
|
||||
x: {
|
||||
ticks: { display: false, color: '#9CA3AF', maxRotation: 0, minRotation: 0 },
|
||||
ticks: { display: false, color: axisTickColor, maxRotation: 0, minRotation: 0 },
|
||||
grid: { display: false },
|
||||
},
|
||||
},
|
||||
@@ -294,15 +288,15 @@ const memoryChartOptions = ref<ChartOptions<'line'>>({
|
||||
min: 0,
|
||||
// max will be set dynamically based on memTotal
|
||||
ticks: {
|
||||
color: '#9CA3AF',
|
||||
color: axisTickColor,
|
||||
callback: function(value) {
|
||||
return `${parseFloat(Number(value).toFixed(1))}`; // Unit will be implicit from title or tooltip
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(156, 163, 175, 0.1)' },
|
||||
grid: { color: axisGridColor },
|
||||
},
|
||||
x: {
|
||||
ticks: { display: false, color: '#9CA3AF', maxRotation: 0, minRotation: 0 },
|
||||
ticks: { display: false, color: axisTickColor, maxRotation: 0, minRotation: 0 },
|
||||
grid: { display: false },
|
||||
},
|
||||
},
|
||||
@@ -337,7 +331,7 @@ const networkChartOptions = ref<ChartOptions<'line'>>({
|
||||
min: 0,
|
||||
max: 10, // 初始值,将动态更新
|
||||
ticks: {
|
||||
color: '#9CA3AF',
|
||||
color: axisTickColor,
|
||||
callback: function(value) {
|
||||
const precision = networkRateUnitIsMB.value ? 2 : 0; // KB/s usually whole numbers, MB/s two decimal places
|
||||
// For KB/s, if the value is very small (e.g. < 1), it might be better to show 1 decimal.
|
||||
@@ -349,10 +343,10 @@ const networkChartOptions = ref<ChartOptions<'line'>>({
|
||||
return `${Number(value).toFixed(precision)}`;
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(156, 163, 175, 0.1)' },
|
||||
grid: { color: axisGridColor },
|
||||
},
|
||||
x: {
|
||||
ticks: { display: false, color: '#9CA3AF', maxRotation: 0, minRotation: 0 },
|
||||
ticks: { display: false, color: axisTickColor, maxRotation: 0, minRotation: 0 },
|
||||
grid: { display: false },
|
||||
},
|
||||
},
|
||||
@@ -445,10 +439,87 @@ watch(() => props.serverStatus, () => {
|
||||
updateAxisAndUnits();
|
||||
}, { deep: true, immediate: true }); // immediate: true 确保初始加载时设置好轴
|
||||
|
||||
// 移除监听 activeSessionId 的 watcher 和 resetChartData 函数
|
||||
</script>
|
||||
|
||||
onMounted(() => {
|
||||
// 初始轴和单位设置由 watch immediate 处理
|
||||
});
|
||||
<style scoped>
|
||||
.status-charts {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 2px;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
</script>
|
||||
.chart-panel {
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.12);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(18, 24, 31, 0.94), rgba(14, 18, 24, 0.94)),
|
||||
linear-gradient(90deg, rgba(52, 211, 153, 0.06), transparent);
|
||||
padding: 14px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||
0 10px 24px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.chart-panel__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chart-panel__title {
|
||||
margin: 0;
|
||||
color: #f7fbff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chart-panel__subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #8fa0b3;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chart-panel__badge {
|
||||
display: inline-flex;
|
||||
min-height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.chart-panel__badge--cpu {
|
||||
border: 1px solid rgba(59, 130, 246, 0.18);
|
||||
background: rgba(37, 99, 235, 0.18);
|
||||
color: #bfdbfe;
|
||||
}
|
||||
|
||||
.chart-panel__badge--network {
|
||||
border: 1px solid rgba(16, 185, 129, 0.18);
|
||||
background: rgba(5, 150, 105, 0.16);
|
||||
color: #bbf7d0;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
height: 164px;
|
||||
}
|
||||
|
||||
@container (min-width: 860px) {
|
||||
.status-charts {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 420px) {
|
||||
.chart-panel__header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user