- 重写 AgentView.vue:运行状态 / 环境与依赖 / frps 控制 / Agent 生命周期 / 输出与日志 五段分区 - 补齐 5 处「数据已返回但页面未展示」:Agent 凭据状态、8080 端口、反代路由数、最后检查时间、Nginx 可用性 - 状态机:busy 按动作粒度 loading、轮询静默、切换服务端重置状态、三态区分(未加载/加载中/失败) - 交互:卸载(含数据)二次确认;管理面板与安装 frps 常显但按条件禁用;输出改为带来源与时间的时间线 - 新增「配置查看」按钮 + 只读弹窗(getFrpFRPSConfig) - 无障碍与响应式:section[aria-label]、aria-live、aria-hidden、320px、≤760px 触控目标、prefers-reduced-motion - 文档:docs/agent-view-design-v2.md(设计规格)、docs/agent-view-design.md、docs/agent-view-refactor-plan.md
16 KiB
公网 Agent 页(AgentView.vue)设计方案 v2
本文按
breezell-design两遍流程产出,核心是先列清功能、再谈设计: 第二节把本页应有的 27 项能力逐条落位(含 5 处当前页面看不到的能力),第四节按功能分区。取代
docs/agent-view-design.md(v1 只做了结构搬家,未解决"功能不可见"问题); 也取代docs/agent-view-refactor-plan.md。更新日期:2026-09-23
一、Pass 1a:栈与设计系统检测
| 检测项 | 实际值 |
|---|---|
| 框架 | Vue 3 + <script setup lang="ts"> + Vite 6 |
| 组件库 | naive-ui(n-button n-select n-tabs n-modal n-popconfirm n-alert n-spin n-icon n-tag n-tooltip n-form) |
| 图标 | @vicons/ionicons5 |
| 令牌系统 | web/src/styles/variables.css::root + html.dark 双主题,--fonu-* 前缀 |
| 共享组件 | PageHeader FonuCard StatCard StatusBadge EmptyState LoadError |
| 工具 | utils/format.ts(formatBytes/formatRate/formatDate/formatRelativeTime/…)、utils/status.ts(StatusKind) |
结论:沿用既有体系(naive-ui + --fonu-* + 共享组件),不引入第二套框架/令牌。
二、功能总清单(Capability Inventory)
设计原则:没有隐式功能。每项能力都要有固定位置、可见状态、明确反馈; 能做的显示按钮,不能做的显示原因(禁用 + 提示),而不是隐藏。
| # | 能力 | 数据来源 | 当前状态 | 新设计位置 | 可见性形态 |
|---|---|---|---|---|---|
| A | 选择公网服务端 | listFrpServers(agent/ssh 已配置) |
已有 | PageHeader#actions |
下拉选择 |
| B | Agent 连通性 | getFrpAgentStatus 成败 |
已有 | ① 运行状态 | 三态:在线 / 未连接 / 未加载 |
| C | Agent 地址 | server.agent_url |
已有 | ① 运行状态卡副标题 | 等宽文本 |
| D | Agent 版本 | agent_version |
已有 | ② 环境与依赖 | 等宽文本 |
| E | Agent 凭据是否已配置 | server.agent_configured |
未展示 | ② 环境与依赖 | StatusBadge:已配置 / 未配置 |
| F | frps 运行状态 | frps_active |
已有 | ① 运行状态 | 在线 / 未运行 |
| G | frps 已安装版本 | frps_version |
已有 | ① + ② | 文本 / 徽标 |
| H | frps 二进制路径 | frps_binary |
已有 | ② 环境与依赖 | 等宽 + 换行 |
| I | frps 配置文件路径 | frps_config_path |
已有 | ② 环境与依赖 | 等宽 + 换行 |
| J | frps 启动命令 | frps_unit_exec |
已有 | ② 环境与依赖 | 等宽 + 换行 |
| K | frps 开机自启 | frps_enabled |
已有 | ② 环境与依赖 | StatusBadge |
| L | frps 管理面板入口 | dashboard_addr/port → dashboardURL |
有(未配置时按钮隐藏) | ③ frps 控制 | 常显;未配置时禁用 + tooltip 指向配置位置 |
| M | 端口监听 80/443/7000/8080 | ports |
仅 80/443 卡 + 7000 字段(8080 未展示) | ① 运行状态「端口」卡 | 四端口全列:80✓ 443✓ 7000✗ 8080✗ |
| N | 已注册反代路由数 | routes[] |
未展示 | ① 运行状态 | 「反代路由 N 条」+ 折叠列表 |
| O | 最后一次检查时间 | checked_at |
未展示 | PageHeader 副标题 |
「检查于 x 前」(formatRelativeTime) |
| P | Nginx 可用性(本页完全未展示) | nginx_ok/missing/running/version/path/conf_dir/error |
未展示(在公网反代页) | ② 环境与依赖「Nginx」组 | 徽标 + 版本 + 路径 + 配置目录 + 错误原文 + 跳转 |
| Q | 启动 / 停止 frps | frpsServerActionRaw(start/stop) |
已有 | ③ frps 控制 | 按状态互斥的按钮 |
| R | 重启 frps | frpsServerActionRaw(restart) |
已有 | ③ frps 控制 | 按钮 |
| S | 下发 frps 配置 | pushFrpConfigToAgent |
已有(散在另一卡) | ③ frps 控制 | 按钮 |
| S2 | 查看即将下发的 frps 配置 | getFrpFRPSConfig |
本页未提供 | ③ frps 控制(紧跟下发按钮) | 按钮 + 只读弹窗(等宽代码块 + 复制) |
| T | 安装 / 升级 frps | installFrpServer + listFrpReleases |
已有 | ③ frps 控制 | 版本下拉 + 按钮(列表空时禁用) |
| U | 探测环境 | probeFrpAgent |
已有 | ④ Agent 生命周期 | 按钮 |
| V | 安装 / 升级 Agent | installFrpAgent |
已有 | ④ Agent 生命周期 | 按钮 |
| W | 卸载(保留数据 / 含数据) | uninstallFrpAgent(keepData) |
已有(危险无确认) | ④ Agent 生命周期 | 两按钮,含数据带二次确认 |
| X | SSH 诊断 | sshDiagnoseFrpAgent |
已有 | ④ Agent 生命周期 | 按钮 |
| Y | frps 最近日志 | frps_log_tail |
已有(独立卡) | ⑤ 输出与日志 Tab | 日志块 + 空态 |
| Z | 操作输出(7 类) | 各接口返回 | 已有(单一 agentOutput) |
⑤ 输出与日志 Tab | 时间线:来源 + 时间 + 成败 |
| AA | 公网反代管理入口 | getFrpAgentRouteCandidates / RouteHealth |
有(整卡只放跳转) | 入口行 + ① 反代路由卡 | 「N 条规则 · 健康 X/Y」+ 跳转 /public-proxy |
本次必须补齐的 5 处(数据已返回,页面没显示)
| 项 | 数据字段 | 为什么重要 |
|---|---|---|
| E | agent_configured |
决定"能不能连上 agent",是排障第一问 |
| M(8080) | ports["8080"] |
frps vhost 端口是否在听,直接对应反代回源 |
| N | routes[] |
一眼看出 VPS 上已注册几条反代 |
| O | checked_at |
判断状态是不是陈旧的(页面 10 秒轮询,用户需要知道数据新鲜度) |
| P | nginx_* 七字段 |
反代的前置依赖;本页不显示就只能跳到另一页才能发现"没装 Nginx" |
三、设计方向与令牌(Pass 1b)
方向:机架面板(controlled density)——像看一排机架指示灯:状态永远第一眼可见,操作收进分区按钮带,输出以时间线呈现;克制、无装饰、密度优先。
- Layout:状态区(4 列稳定网格)→ 环境与依赖(两列)→ 控制/生命周期(两列)→ 输出与日志(整宽);左对齐,无居中大标题、无渐变、无装饰插图。
- Signature:四色状态点行 + 操作输出时间线。
Color(5 个语义令牌,全部复用现有)
| 语义 | 令牌 | 用于 | 判定 |
|---|---|---|---|
| 正常 | --fonu-brand(-soft/-text) |
在线、运行中、监听中、成功输出 | 无需干预 |
| 待处理 | --fonu-warning |
frps 未运行、端口未监听、frps 未安装、开机自启未启用 | 可一键恢复 |
| 需人工干预 | --fonu-error |
Agent 未连接、Nginx 缺失、操作失败、卸载确认 | 需排查/不可逆 |
| 中性 | --fonu-text-secondary / --fonu-text-muted |
字段名、说明、时间、空态 | — |
| 引导 | --fonu-info(-soft) |
页面级引导(反代入口等) | 非故障 |
规则:一屏 error ≤ 1 处;warning 只用于"有目标状态但未达成",不用于"没数据"。亮/暗由 variables.css 覆盖,页面零硬编码色值。
Type(沿用项目字体,显式声明)
- 正文
--fonu-font,等宽--fonu-mono。skill 禁用 system-ui 系默认栈,但本项目已有设计系统,按 "MATCH it, never add a second" 继续使用;换字体属全局决策(改variables.css+ 全站回归),不在本页范围。 - 页内 4 档字号 / 3 档字重:数值词条 18px/700(
.stat-card__value--sm)、数值数字 30px/700/-0.03em、字段名与卡标签 12–13px/600、说明与时间 12–12.5px/400;路径/命令/版本/日志走--fonu-mono12.5–13px。
Spacing & Radius:只用 --fonu-space-1…6 与 --fonu-radius(-sm)、--fonu-shadow;禁止魔数;内联尺寸一律改类(.agent-select / .frps-version-select)。
四、页面分区与线框(按功能分区)
┌ PageHeader 公网 Agent · 检查于 12 秒前 [服务端 ▾] [刷新] ┐
├ n-alert 状态提示(互斥一条:error 未连接 / warning frps 未运行)
├ 入口行 公网反代:3 条规则 · 健康 2/3 → /public-proxy
├ ① 运行状态 section[aria-label=运行状态] 4 列稳定网格
│ ● Agent 连接 ● frps 服务 ● 反代路由 ● 端口监听
│ 在线/未连接 运行中/未运行 3 条 80✓ 443✓
│ <agent 地址> <frps 版本> 健康 2/3 已生效 7000✓ 8080✗
├ ② 环境与依赖(Fonu Agent | Nginx | frps 运行时,三列并排)
│ Fonu Agent Nginx(本页新增) frps 运行时
│ 版本 / 地址 可用性徽标 / 版本 二进制 / 版本
│ 凭据:已配置 ✓ 可执行路径 / 配置目录 配置路径
│ [探测环境] 错误原文(红) 启动命令 / 开机自启
│ → 去「公网反代」处理
├ ③+④ 两列并排(左:Agent 生命周期(SSH) / 右:frps 控制)
│ 左 [探测环境] [安装/升级 Agent] [SSH 诊断] 右 [启动|停止] [重启] [管理面板]
│ [卸载(保留数据)] [卸载(含数据) ← 二次确认] [下发配置] [版本 ▾] [安装/升级]
└ ⑤ 输出与日志 n-tabs:操作时间线(来源+时间+成败,aria-live) | frps 日志
空态 EmptyState · [清空]
分区原则:
- 每项能力都有固定位置(对照第二节清单);按钮常显,不可用时禁用 + 说明原因,绝不隐藏;
- 状态区只读 / 控制区只写 / 输出区只展示,三段物理分离;
- 新增的「环境与依赖」把三件前置条件(Agent 凭据、Nginx、frps 运行时)集中可见——排障时最先要看的就是这三项。
五、组件与状态矩阵(Pass 2 规格)
| 元素 | default | hover | pressed | focused | disabled | loading |
|---|---|---|---|---|---|---|
n-button(主/次/危险) |
库默认 | 库默认 | 库默认 | 库默认焦点环(键盘实测) | busy !== null && busy !== 本动作 |
busy === '动作名' |
| 服务端选择器 | 库默认 | 库默认 | — | 库默认 | 无服务端时禁用 | — |
n-tabs(输出/日志) |
库默认 | 库默认 | 库默认 | 库默认(←/→ 可切换) | — | — |
n-popconfirm(卸载含数据) |
warning 触发器 | 库默认 | 库默认 | 库默认 | busy 时禁用 | 确认后走按钮 loading |
.stat-card(只读) |
令牌底色 | 有意不加 hover | — | 非交互不可聚焦 | — | 骨架 / — |
| 「管理面板」(未配管理接口) | 常显但禁用 | — | — | — | 是(tooltip 指向配置位置) | — |
| 「安装/升级 frps」(版本列表未加载) | 常显但禁用 | — | — | — | 是(提示「正在获取官方版本列表…」) | 列表加载中 |
内容状态(区分"没数据"与"拿不到数据")
| 场景 | 呈现 |
|---|---|
| 尚未拉取 | 数值位 —,副标题「读取中…」 |
| 首次拉取中 | n-spin 包裹状态区(仅首次,轮询不显示) |
| 拉取失败 | 数值位「未连接」+ 顶部 error alert(含「探测环境」动作) |
| 无已注册路由 / 无日志 | EmptyState(不再渲染「暂无日志」字符串) |
| 操作失败 | 时间线中该条 ok=false 标红 + message.error |
六、动效与无障碍
动效:首屏一次性 fade-up(translateY(4px)→0,160ms),仅 onMounted 触发一次;prefers-reduced-motion: reduce 全禁用;除此之外不做散点微动效(不加卡片 hover 位移/脉冲)。
无障碍:状态区 <section aria-label="运行状态">;装饰图标 aria-hidden;按钮均有可读文本;输出区 aria-live="polite";Tab 顺序 = 选择器 → 刷新 → 状态区(跳过只读)→ 各分区按钮带 → tabs;对比度实测 AA(--fonu-brand/warning/error/text-muted 在 --fonu-surface 上,亮+暗各一遍);≤760px 按钮 min-height: 44px;响应式 1200+ / 900–1199 / ≤900 / ≤760(运行状态 4→2→1 列;环境与依赖三列 →≤1199 降 2 列 →≤900 单列,且三列时卡片内短字段(版本/徽标)双列并排、长路径与命令整行,控制卡片高度),320px 无横向溢出。
七、状态与数据流(含前一轮复核发现的 3 处缺陷修复)
// 1) 切换服务端必须重置,避免串台
watch(selectedId, () => {
opOutput.value = []
agentStatus.value = null
statusLoaded.value = false
agentStatusOk.value = false
void refreshAll()
})
// 2) 三态区分:未加载 / 加载中 / 失败
const statusLoaded = ref(false) // 是否成功拉取过
const statusLoading = ref(false) // 首次加载中
// 3) 按动作粒度 loading,轮询静默
type Action = 'probe' | 'agent-install' | 'agent-uninstall' | 'diagnose'
| 'frps-start' | 'frps-stop' | 'frps-restart' | 'frps-install' | 'push-config'
const busy = ref<Action | null>(null)
async function run<T>(action: Action, fn: () => Promise<T>) {
if (busy.value) return // 安全网:按钮已 disabled
busy.value = action
try { return await fn() } finally { busy.value = null }
}
async function refreshAll(options: { silent?: boolean } = { silent: true }) { /* 轮询不触碰 busy */ }
// 4) 输出时间线:来源 + 时间 + 成败,上限 20 条
interface OpOutput { at: string; source: string; text: string; ok: boolean }
const opOutput = ref<OpOutput[]>([])
function pushOutput(source: string, text: string, ok = true) {
opOutput.value = [{ at: new Date().toISOString(), source, text, ok }, ...opOutput.value].slice(0, 20)
}
提示规则:状态提示互斥且最多一条(error 未连接 > warning frps 未运行);「公网反代入口」不占 alert 位,独立成入口行。
轮询:10 秒;document.visibilityState === 'hidden' 时跳过;onBeforeUnmount 清理定时器。
八、本页不做(Never)
- 不引入第二套 UI 框架 / 令牌 / 全局样式;不改全局字体与令牌;
- 不用硬编码色值与魔数间距;不做渐变、居中大标题、统一灰卡片等 slop 模式;
- 统计卡不加装饰性 hover / 位移 / 脉冲;
- 不把页面级操作塞进正文卡片(统一进
PageHeader#actions); - 不隐藏功能:任何能力要么可用,要么可见地禁用并给出原因;
- 不改后端与 agent(纯前端单文件)。
九、验收清单
- 复用项目真实栈与令牌(naive-ui +
--fonu-*+ 共享组件),未新增第二套 - 单一设计方向(机架面板)落地,颜色/间距/圆角全走令牌,无 banned 字体引入
- 交互状态齐全:hover / pressed / focused(键盘实测) / disabled / loading / empty / error
- 无障碍:对比度 AA(亮+暗)、可见焦点环、键盘可完成全部操作、
aria-live播报、图标aria-hidden - 最窄 320px 无横向溢出;≤760px 按钮 ≥44px
- 功能可见性:逐条对照第二节 A–AA,每项都有明确位置;E / M(8080) / N / O / P 五项新增展示已落地;不存在隐藏功能
- 回归:切换服务端不串台;仅被点按钮 loading;轮询不闪;危险操作有二次确认;
vue-tsc0 错误 +vite build成功
十、与既有文档关系与工作量
| 文档 | 关系 |
|---|---|
docs/agent-view-design.md(v1) |
被本文取代(v1 只是结构搬家,未解决"功能不可见");确认后可删除 |
docs/agent-view-refactor-plan.md |
被本文取代 |
工作区 skill fonu-page-ui |
页面 UI 通用标准;本文是它在「公网 Agent」页的具体化 |
工作量:单文件改动(web/src/views/AgentView.vue),约 5–6 小时(含 5 处能力补齐、状态机重构、无障碍与响应式)。
验证:npx vue-tsc --noEmit + npx vite build,再同步 web/dist → cmd/fonu/web/dist 后 go build ./...。