Files
-Fonu/docs/proxy-remote-design.md
T
mrlee 5b5091e1ed feat(web): 公网反代页重构(部署台账 + 弹窗对齐本地页)
- 台账改用 n-data-table:规则/域名(含复制与打开)、协议、上游/回源、健康、连接、当前上传、当前下载、总上传、总下载、更新时间、操作
- 单元格样式走 :deep():render 函数创建的节点不带 scoped 的 data-v 属性
- 修复 loadErrorMsg 从不渲染、按动作粒度 loading、切换服务端重置、轮询静默与可见性保护、移除反代二次确认
- 部署设置弹窗对齐本地反代页:900px、三段式、页脚分隔线、开关行底色,并新增右侧配置说明栏
- 新增统计区(可部署规则/已生效/健康通过/当前连接)与环境与 Nginx 卡(4 列)
- 文档:docs/proxy-remote-design.md
2026-09-23 15:22:43 +08:00

214 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 公网反代页(`ProxyRemoteView.vue`)设计方案
> 目标:以**本地反向代理页 `ProxyView.vue` 为交互参照**,结合公网反代特有语境(部署到 VPS、Nginx 由 agent 管理、证书由 agent 推送、frps 提供隧道),
> 把公网反代页从"能用的列表页"整理成**功能齐全、状态可见、与本地页同族**的页面。
>
> 按 `breezell-design` 两遍流程产出:Pass 1a 栈检测 → Pass 1b 设计方向与令牌 → Pass 2 实施规格。
> 参考页:`ProxyView.vue`(3485 行,本地反代)、`AgentView.vue`(刚按 v2 重构,本页与其保持同族)。
> 更新日期:2026-09-23
---
## 一、Pass 1a:栈与设计系统检测
| 检测项 | 实际值 |
| --- | --- |
| 框架 | Vue 3 + `<script setup lang="ts">` + Vite 6 |
| 组件库 | **naive-ui**(`n-select` `n-button` `n-modal` `n-form` `n-switch` `n-input` `n-input-number` `n-tag`) |
| 图标 | `@vicons/ionicons5`(本页目前未用图标) |
| 令牌系统 | `web/src/styles/variables.css`:`:root` + `html.dark` 双主题,`--fonu-*` 前缀 |
| 共享组件 | `PageHeader` `FonuCard` `LoadError` `EmptyState` `StatusBadge`(本页**未用** `StatusBadge`) |
| 工具 | `utils/format.ts`(已用 `formatBytes` / `formatRate`) |
| 同族页面 | `ProxyView`(本地反代,交互参照)、`AgentView`(已按 v2 重构,同为公网侧) |
**结论**:沿用既有体系(naive-ui + `--fonu-*` + 共享组件),**不引入**第二套框架/令牌。
---
## 二、现状盘点(含实测缺陷)
`ProxyRemoteView.vue` 现为 584 行,结构是:`PageHeader` → Nginx 提示条 → 三张卡片(Nginx 状态 / 穿透规则与公网反代状态 / 使用说明)→ 两个弹窗(部署设置 / 反代配置)。
| # | 问题 | 证据 | 影响 |
| --- | --- | --- | --- |
| 1 | **刷新失败静默** | `refreshAll()` 的 catch 写入 `loadErrorMsg`,但该变量**从未在模板中渲染** | 拉取规则/健康/流量失败时页面毫无反馈,用户以为"就是没数据" |
| 2 | **没有统计区** | 全页无 `.stats-row` / `.stat-card`,而数据齐全(规则数、已部署数、健康通过数、流量、连接数) | 与所有标准页(Dashboard/Ddns/Certificates/Proxy)不一致,一眼看不到全局 |
| 3 | **硬编码颜色 6 处** | `#f0c36d`、`#e79b9b`、`#dc2626`、`#16a34a`、`rgba(240,195,109,.12)`、`rgba(34,197,94,.12)` | 违反令牌规则;暗色主题下对比度未验证 |
| 4 | **自造状态组件** | `nginxState()` 返回自造 `n-tag`;`healthBadges()` 自造 `.health-badge/.health-ok/.health-bad` | 与 `StatusBadge` + `utils/status.ts` 语义分叉,同类状态在不同页面长得不一样 |
| 5 | **「使用说明」整卡占位** | 一整张 `FonuCard` 只有一段 `form-hint` | 占掉整行网格,信息密度低(`AgentView` 已把这类内容改成 `n-alert` + 入口行) |
| 6 | **空态与折叠区未令牌化** | 自造 `.empty-route` div;`<details class="raw-routes">` 原生折叠 | 空态应为 `EmptyState`;原生折叠样式不随主题 |
| 7 | **loading 粒度粗** | 单个 `refreshing` 同时驱动「刷新」「重新检测」与轮询 | 轮询时按钮闪 loading;无法区分"在部署"与"在刷新" |
| 8 | **切换服务端不重置** | `watch(selectedId)` 不存在 | 上一台的健康/流量/agent 状态残留在新服务端视图里(串台) |
| 9 | **轮询未做可见性/冲突保护** | 15 秒 `setInterval` 里只判断 `!refreshing` | 页面隐藏时仍打 6 个接口;`refreshAll` 串行 6 请求,慢时可能叠压 |
| 10 | **内联尺寸** | `style="width: 220px"` | 应为类名(如 `AgentView` 的 `.agent-select`) |
| 11 | **能力缺口(本地页有、本页无)** | 本地页有:批量排序(`reorderProxies`)、扫描发现(`scanDiscovery`)、Nginx 配置编辑与**回滚**(`saveProxyNginx`/`rollbackProxyNginx`)、客户端连接列表(`getProxyClients`)、实时流量曲线 | 公网侧对应位置缺失:无批量部署、无历史回滚、无已部署路由的客户端/连接明细视图 |
---
## 三、功能总清单(Capability Inventory)
> 原则同 `AgentView` v2:**没有隐式功能**——每个能力有固定位置、可见状态、明确反馈;能做的显示按钮,不能做的显示原因(禁用 + 提示)。
| # | 能力 | 数据来源 | 现状 | 新设计位置 | 可见性形态 |
| --- | --- | --- | --- | --- | --- |
| A | 选择公网服务端 | `listFrpServers`(agent/ssh 已配置) | 已有 | `PageHeader#actions` | 下拉 + 类名宽度 |
| B | 刷新全部数据 | `refreshAll()` | 已有 | `PageHeader#actions` | 主按钮 + `:loading`(独立于轮询) |
| C | **页面级状态提示** | `agentStatus.nginx_missing / nginx_ok / nginx_error` | 自造 alert(硬编码色) | `PageHeader` 之后 | `n-alert`(error/warning),含动作按钮 |
| D | Nginx 可用性概览 | `nginx_missing/ok/running/version/path/conf_dir/error` | 8 项 `nginx-status-grid` | ① 环境区(与 AgentView 同族) | 徽标 + 值字段(令牌化) |
| E | 一键安装 Nginx | `installFrpAgentNginx` | 已有(提示条 + 卡片各一处,重复) | ① 环境区(**只保留一处**) | 按钮 + `:loading` |
| F | **规则汇总统计** | 由 `routeCandidates` + `routeDetails` + `routeHealth` 计算 | **无** | ② 统计区(4 列) | 统计卡:规则总数 / 已生效 / 健康通过 / 连接数 |
| G | 规则列表(每行) | `getFrpAgentRouteCandidates` | 已有 | ③ 规则区 | 行:名称 + 域名 + 上游 + 回源端口 |
| H | 部署态徽标(四态) | `getFrpAgentRoutes` → `stateOf()` | 已有(自造 tag) | ③ 规则行 | `StatusBadge`:已生效 / 已写入未加载 / 语法错误 / 未部署 |
| I | 健康检查(4 段) | `getFrpAgentRouteHealth` → DNS/证书/隧道/服务 | 已有(自造 badge) | ③ 规则行 | `StatusBadge` × 4 + 悬停详情 |
| J | 流量指标(6 项) | `listFrpProxiesTraffic` | 已有(自造 `.traffic-metric`) | ③ 规则行 | 等宽小字 + `formatBytes/formatRate`;采集失败标红 |
| K | 部署反代 | `deployFrpAgentRoute` | 已有 | ③ 规则行 | 主按钮(仅该行 loading) |
| L | 移除反代 | `removeFrpAgentRoute` | 已有 | ③ 规则行 | warning 按钮 + **二次确认**(现无确认) |
| M | 部署设置(3 Tab) | `updateFrpProxy` + 重新部署 | 已有(字段齐全) | 弹窗(沿用 `proxy-modal` 结构) | 基础 / 安全 / 高级 |
| N | 查看生成的 nginx 配置 | `getFrpAgentRouteConf` | 已有 | ③ 规则行 → 弹窗 | 只读代码块 + 复制 |
| O | **VPS 上 agent 管理的路由文件** | `getFrpAgentRoutes`(含 syntax_ok/loaded) | `<details>` 折叠 | ③ 规则区底部 | 与部署态合并展示,令牌化折叠(或独立 Tab) |
| P | 空态引导 | — | 自造 `.empty-route` | ③ 规则区 | `EmptyState` + 「去内网穿透页」动作 |
| Q | 前往公网 Agent 页 | — | 无入口 | `PageHeader` 下方入口行 | 次级链接(Nginx/agent 异常时指向排查页) |
| R | **批量部署**(能力缺口) | 循环 `deployFrpAgentRoute` | **无** | ③ 规则区头部 | 「部署全部未部署」+ 进度反馈(**分批实施,见第十一节**) |
| S | **配置回滚**(能力缺口) | agent 侧需新增历史版本接口 | **无** | 弹窗内(部署设置 → 高级)或规则行 | 暂列"不做",需后端支持(见第九节) |
---
## 四、与本地反代页(`ProxyView.vue`)的对照
| 本地页能力 | 公网反代对应做法 | 结论 |
| --- | --- | --- |
| 6 张统计卡(服务总数/总上传/总下载/当前上传/当前下载/当前连接) | 公网侧统计对象是**规则与部署态**,不是单机流量 | **对齐形式**(`.stats-row` + `.stat-card`),**换指标**:规则总数 / 已生效 / 健康通过 / 连接数 |
| 概览区:域名 · 监听 · 目标 · 协议(`overview-strip`) | 公网侧对应"这台 VPS 的入口":域名 / 回源 / vhost 端口 / 协议 | **对齐**:放进规则行的上游行,不单开概览卡 |
| 实时流量卡(最近 5 分钟 + `live-badge` 实时) | frps 侧只有**当日累计 + 相邻两次采样速率**,无时间序列接口 | **不对齐**:保留"当前速率"文本,不做曲线;若要做需后端提供采样序列 |
| 客户端连接列表(`getProxyClients`) | 公网侧无对应接口(frps dashboard 只给连接数) | **不对齐**:显示连接数;明细需后端扩展 |
| Nginx 配置编辑 + 保存 + **回滚** | 公网侧配置由 agent 生成(模板化),手工编辑会与模板冲突 | **不对齐(有意)**:只提供"查看配置";回滚需 agent 侧版本化,列入不做 |
| 扫描发现(`scanDiscovery`)、排序(`reorderProxies`) | 公网侧规则来源是穿透规则(内网侧),排序/发现属内网语义 | **不对齐**:排序按"名称 + 域名"稳定排序;发现入口指向「内网穿透」页 |
| 规则弹窗 3 Tab(基础配置/安全设置/Nginx) | 公网侧弹窗已同构(基础/安全/高级) | **已对齐**(本轮不改字段,只做校验与提示) |
| 表单里「使用目标 Host 头」「忽略后端 TLS 校验」 | 公网侧明确不做(破坏 frps vhost 路由 / 回源为明文) | **不照搬**(已写入 `docs/agent-proxy-settings-plan.md` 排除清单) |
---
## 五、设计方向与令牌(Pass 1b)
**方向:部署台账(deployment ledger)**——与 `AgentView` 的"机架面板"同族:一行一条规则,左边是"这条反代是什么",右边是"它现在什么状态、能做什么";颜色只用来表达状态,不用来装饰。
- **Layout**:状态提示 → 统计区(4 列)→ 环境与 Nginx(2 列)→ 规则台账(整宽)→ 入口行;左对齐,无居中、无渐变。
- **Signature**:**规则行左侧的「四段健康徽标串」**(DNS · 证书 · 隧道 · 服务),一眼扫完所有规则的健康。
**Color**:沿用 5 个语义令牌(同 `AgentView` v2):`--fonu-brand` 正常 / `--fonu-warning` 待处理 / `--fonu-error` 需人工干预 / 中性 `--fonu-text-*` / `--fonu-info` 引导。**页面零硬编码色值**(现状 6 处全部替换:`#16a34a`→`--fonu-brand`、`#dc2626`→`--fonu-error`、`#f0c36d`→`--fonu-warning`、`#e79b9b`→`--fonu-error`、两处 `rgba(...)`→`--fonu-brand-soft` / `--fonu-warning` 的 soft 变体或 `--fonu-bg-muted`)。
**Type**:沿用 `--fonu-font` / `--fonu-mono`;页内 4 档字号(12 / 12.5 / 13 / 18-30)+ 3 档字重,与 `CertificatesView` 统计卡规格一致。
**Spacing & Radius**:只用 `--fonu-space-1…6` 与 `--fonu-radius(-sm)`、`--fonu-shadow`;`style="width: 220px"` → `.proxy-remote-select`。
---
## 六、页面分区与线框
```text
┌ PageHeader 公网反代 · 一句话说明 [服务端 ▾] [刷新] ┐
├ n-alert(Nginx 缺失 / 校验失败,含「一键安装 Nginx」动作)
├ 入口行:Agent 与 frps 的生命周期、配置下发在「公网 Agent」页 →
├ ① 统计区(4 列) 规则总数 · 已生效 · 健康通过 · 当前连接
├ ② 环境与 Nginx(2 列)
│ 可用性 / 进程 / 版本 / 可执行文件 / vhost 目录 / 配置校验 / 端口监听
├ ③ 规则台账(整宽)
│ [名称] 域名(mono) [已生效] [查看配置] [部署设置] [部署反代] [移除反代]
│ 上游 192.168.31.152:7023(回源 127.0.0.1:8080)
│ DNS ✓ 证书 ✓ 隧道 ✓ 服务 ✓ 连接 3 · 当前 ↑12KB/s ↓2MB/s · 总 ↑1.2GB ↓8.4GB
│ …(每行一条;空态 EmptyState + 「去内网穿透页」)
│ 折叠:VPS 上 agent 管理的路由文件(3)—— 7023.36800.cc 已加载 / …
└ 弹窗:部署设置(基础 / 安全 / 高级)· 反代配置(只读 + 复制)
```
分区原则:**统计区只读汇总 / 环境区只读诊断 + 安装动作 / 规则区一行一规则(状态在左、动作在右)**;「使用说明」整卡删除,内容并入 `PageHeader.description` 与 `n-alert`。
---
## 七、组件与状态矩阵(Pass 2 规格)
| 元素 | default | hover | pressed | focused | disabled | loading |
| --- | --- | --- | --- | --- | --- | --- |
| 刷新按钮 | 库默认 | 库默认 | 库默认 | 库默认焦点环 | 无服务端时禁用 | `:loading="manualRefreshing"`(轮询不驱动) |
| 服务端选择器 | 库默认 | 库默认 | — | 库默认 | 无服务端时禁用 | — |
| 「部署反代」 | primary | 库默认 | 库默认 | 库默认 | 该行操作进行中 | `busy === 'deploy:<id>'` |
| 「移除反代」 | warning quaternary | 库默认 | 库默认 | 库默认 | 同上 | `busy === 'remove:<id>'` |
| 「一键安装 Nginx」 | primary | 库默认 | 库默认 | 库默认 | agent 未连接 | `:loading="installingNginx"` |
| 健康徽标 / 部署态 | `StatusBadge` | —(只读) | — | 不可聚焦 | — | 数据未加载时显示 `—` |
| 统计卡 | 令牌底色 | **有意不加 hover** | — | 不可聚焦 | — | 未加载显示 `—`(不是 0) |
| 折叠「路由文件」 | 原生 summary + 令牌样式 | 文字色变 | — | 库默认 | — | — |
**内容状态**:未加载 `—` / 首次加载 `n-spin` / 拉取失败 `n-alert error`(修 #1,`loadErrorMsg` 必须渲染)/ 无规则 `EmptyState` / 无路由文件 折叠区隐藏 / 流量采集失败该行标红并给 tooltip。
---
## 八、状态与数据流(含缺陷修复)
```ts
// 1) 切换服务端必须重置(修 #8)
watch(selectedId, () => {
routeCandidates.value = []; routeDetails.value = []; routeHealth.value = []
proxyTraffic.value = {}; trafficErrors.value = {}; agentStatus.value = null
loadErrorMsg.value = ''
void refreshAll({ silent: false })
})
// 2) 错误必须可见(修 #1)
const loadErrorMsg = ref('') // 模板:<n-alert v-if="loadErrorMsg" type="error" :bordered="false">{{ loadErrorMsg }}</n-alert>
// 3) loading 分粒度(修 #7)
const manualRefreshing = ref(false) // 仅"刷新/重新检测"按钮
type Action = `deploy:${number}` | `remove:${number}` | 'install-nginx' | 'save-settings'
const busy = ref<Action | null>(null)
async function run<T>(action: Action, fn: () => Promise<T>) { /* 同 AgentView:串行 + 失败写入 loadErrorMsg */ }
// 4) 轮询:静默 + 可见性保护(修 #9)
timer = window.setInterval(() => {
if (!server.value || document.visibilityState === 'hidden') return
void refreshAll({ silent: true }) // 不触碰 manualRefreshing,不覆盖 loadErrorMsg(失败仅记 console + 顶部提示去抖)
}, 15000)
```
`refreshAll` 的 6 个接口调用保持串行(`routeCandidates → routeDetails → routeHealth → traffic → status`),但**单个接口失败不再中断整轮**:分别 try/catch,把失败项汇总成一句提示(现状是一个失败就整轮丢失后续数据)。
---
## 九、本页不做(Never)
- 不引入第二套 UI 框架 / 令牌;不改全局字体与令牌;
- 不用硬编码色值(现状 6 处全部清除)与魔数间距;
- **不做** Nginx 配置手工编辑与回滚(公网配置由 agent 模板生成,手工编辑会与模板冲突;回滚需 agent 侧版本化,属后端能力);
- **不做**流量时间序列曲线(frps 只提供当日累计 + 采样速率,无历史接口);
- **不做**目标 Host 头改写、忽略后端 TLS 校验(会破坏 frps vhost 路由 / 回源是明文);
- 不把「使用说明」这类静态文案单独占一张卡;
- 不在本页重复 Agent 生命周期操作(安装/升级/卸载 agent 归「公网 Agent」页,本页只放入口链接)。
---
## 十、验收清单
- [ ] 复用项目真实栈与令牌(naive-ui + `--fonu-*` + `PageHeader/FonuCard/LoadError/EmptyState/StatusBadge`),未新增第二套
- [ ] 单一设计方向(部署台账)落地;零硬编码色值;`style="width: …"` 清零
- [ ] 交互状态齐全:hover / pressed / focused(键盘实测)/ disabled / loading / empty / error
- [ ] 无障碍:对比度 AA(亮+暗,重点验 `--fonu-brand` / `--fonu-warning` / `--fonu-error` 在 `--fonu-surface` 上)、键盘可达全部动作、装饰图标 `aria-hidden`、健康徽标有可读文本
- [ ] 最窄 320px 无横向溢出;≤760px 规则行纵向堆叠、动作按钮铺满且 ≥44px
- [ ] **功能可见性:对照第三节 A–Q,每项有明确位置;#1 错误可见、#8 切换不串台、#7 loading 分粒度 三项缺陷已修**
- [ ] 回归:部署/移除/保存部署设置三条路径真机验证;`vue-tsc` 0 错误 + `vite build` 成功
---
## 十一、实施顺序与工作量
| 阶段 | 内容 | 预计 |
| --- | --- | --- |
| P1 缺陷修复 | #1 错误可见、#8 切换重置、#7 loading 分粒度、#9 轮询保护、#10 内联尺寸、#3 硬编码色值 | 1h |
| P2 结构对齐 | 统计区(4 卡)、环境与 Nginx 卡令牌化、删「使用说明」卡改 `n-alert` + 入口行、空态换 `EmptyState` | 1.5h |
| P3 规则行重排 | `StatusBadge` 化部署态与健康徽标、流量指标排版、移除二次确认、折叠区令牌化 | 1.5h |
| P4 弹窗收尾 | 部署设置字段校验与提示统一、反代配置弹窗加「复制」 | 0.5h |
| P5 验证 | tsc / build / 真机回归(部署·移除·保存设置) | 1h |
合计约 **5.5–6 小时**,单文件改动(`ProxyRemoteView.vue`),**不涉及后端与 agent**。
能力缺口 R(批量部署)与 S(配置回滚)不在本轮:R 可后续单独做(纯前端循环 + 进度),S 需 agent 侧新增历史接口。
验证:`npx vue-tsc --noEmit` + `npx vite build`,再同步 `web/dist → cmd/fonu/web/dist` 后 `go build ./...`。