Files
LokiStack/docs/web-ui-guide/07-组件设计与交互反馈.md
logan f9c6c1d826 feat: opsx-loki 全面升级 — 前端重构/用户与LDAP/部署向导/日志查询
基于 WEB-UI-Development-Guide 标准的整体升级,并更名 LokiDeploy → opsx-loki。

前端(Gin+React+shadcn/ui):
- 按 02/07 章重构:页面域化拆分(无超 400 行文件)、PageContainer/ListQuery、
  反馈分级(成功静默/失败 toast/危险确认)、401 单飞刷新+队列重放
- 暗色模式(next-themes)+ 语义 Token(--primary 品牌色/success/warning/info)
- 部署向导(状态推导四步:环境→存储→Loki→采集,内嵌表单闭环)
- 日志查询页:LogQL 标签构建器/拖拽直方图/日志上下文/Live Tail/查询历史
- 按用户+标签策略的查询授权(服务端强制合并策略)

后端(Go+Gin+JSON DB):
- 简单用户管理(仅 admin,无 RBAC)+ TokenVersion 会话吊销 + 登录 IP 限流
- LDAP 接入:配置(AES 加密存储/hasPassword 脱敏)/测试连接/批量导入/登录
- 存储配置测试连接(SigV4 零依赖)+ 桶不存在自动创建
- Loki 三模式部署修复:SSH 密钥认证、3.x 配置模板、ruler 对象存储嵌套块、
  凭证解密、孤儿 deploying 状态恢复、Grafana 部署(可选默认关)+面板导入
- DaemonSet 采集纯 YAML(去 helm 依赖)+ DNS 自愈回退 ClusterIP + 集群名标签
- 日志查询代理:cluster-svc 经 kube-apiserver proxy/url 直连 + cursor 翻页
- 统一 log 命名空间;部署就绪探活;Grafana 访问地址自动记录

修复:run.sh 增量编译检测、Sanitized 脱敏补全、LokiDeployment Update 白名单、
健康检查 CreatedAt 零值等 10+ 存量 bug。

数据兼容:DB 沿用旧 lokideploy.json(新名优先);localStorage 一次性迁移;
HKDF 加密前缀保留(已加密凭证不受影响)。
2026-10-24 13:20:12 +08:00

12 KiB
Raw Permalink Blame History

07 — 组件设计与交互反馈

参考:vue-vben-admin(Api 类命令式组件)、soybean-admin(hook 优先 + 组件薄壳)、vue-pure-admin(页面内聚五件套 + ReDialog)、ant-design-pro(trigger 弹窗 + ProTable 声明式)


一、组件分层与词汇表

┌─ 全局组件 src/components/ ──────────────────────┐
│  PageContainer  页面容器(页头/面包屑/slot)      │
│  ListPage       分页列表(fetcher+筛选slot+表格) │
│  权限组件 / 图标选择 / 主题切换 / 动态菜单        │
├─ 域组件 views/<domain>/components/ ─────────────┤
│  XxxFormDialog  表单弹窗                         │
│  XxxDetail      详情抽屉                         │
│  单元格渲染器(状态 tag、操作按钮组)              │
├─ 页面骨架 views/<domain>/<Page>.vue ────────────┤
│  只做编排:组合上面两层,≤400 行                  │
└─────────────────────────────────────────────────┘

页面骨架的行数上限是硬约束(400 行)。pure 的同构落地是「页面内聚五件套」:index.vue(纯模板,只解构 hook)+ form/index.vue(弹窗表单)+ 左树/右表子组件 + utils/{hook.tsx, rule.ts, types.ts}(逻辑/校验/类型全在本域)——index.vue 零逻辑。

两种封装哲学(按团队选择,不混用):

哲学 代表 做法
schema 驱动重封装 vben FormApi/ModalApi 类 + schema 数据生成表单/弹层
hook 优先薄壳 soybean 逻辑进 UI 无关 hook(useTable/useFormRules),组件只是组件库薄壳

二、弹层设计:trigger 模式与命令式 Api

2.1 trigger 模式(adp,声明式首选)

// UpdateForm.tsx —— 调用方零状态
<UpdateForm
  trigger={<Button type="link">编辑</Button>}   // 传入触发器
  onFinished={async.value => { actionRef.current?.reload() }}
/>
// 内部 cloneElement(trigger, { onClick: onOpen }) 注入打开事件
// 表单本体 StepsForm + render 进 Modal(destroyOnHidden)

Vue 对应写法:dialog 组件接 trigger slot,或保持「页面持 visible + 子组件 emit close」的朴素方案(够用)。关键是弹窗组件自包含表单逻辑,页面只传初始数据和回调。

2.2 命令式 Api 类(vben,复杂弹层)

// useVbenModal 返回 [Modal, api];ModalApi 持有独立响应式 store
const [Modal, modalApi] = useVbenModal({ connectedComponent: MyModal })
modalApi.open({ payload: { userId: 1 } })   // 命令式打开并传参
modalApi.onConfirm(async () => { ... })     // 生命周期钩子

适用:全局单例弹层(登录过期框、错误确认框——gva 的 ErrorPreview 是此模式的事件总线版)。普通 CRUD 弹窗不必上这套,props/emit 足矣。

Vue 侧最易落地的命令式弹窗是 pure 的 ReDialog:模块级 dialogStore + addDialog({ title, contentRenderer, beforeSure: done => {...} }),done() 关闭——无 provide/inject、无工厂,App.vue 挂载一次全局可用。表单弹窗若要从 props/emit 升级命令式,ReDialog 是阻力最小路线。


三、表单范式:手写、配置驱动与 schema 驱动

范式 适用 代表 成本
手写 el-form + rules 表单结构常变、交互复杂(联动/自定义校验) 常见 Vue 工程 低门槛高纪律
hook 优先(不封表单组件) 逻辑可测、UI 薄壳 soybean useFormRules(satisfies 约束规则表) 中
表单 SFC + 弹窗 contentRenderer 弹窗内容即组件 pure form/index.vue 低
配置驱动 columns 「一列三态」:同一份列配置生成表格列+搜索项+表单项 adp ProTable(valueType/valueEnum/hideInForm/formItemRender) 学习成本中,CRUD 提速极高
schema 驱动 FormApi 表单完全由 schema 数据生成,动态增删字段 vben useVbenForm(zod 校验、响应式 schema) 高,适合平台型产品
RHF + zod React 端 react-hook-form + zod(schema 即校验源) 中

表单通用规则(必须):

  1. 提交按钮 loading 态(防双击重复提交)
  2. 校验消息中文、具体(「请输入用户名」而非「username 不能为空」)
  3. 编辑态回填后 reset 时机明确(打开时回填,关闭时重置)
  4. 危险输入(密码)用 show-password;密钥类只写不读(placeholder 提示「留空则不修改」)

四、表格范式:列表组件是资产

4.1 标准列表组件协议(强制走它)

<ListPage
  :fetcher="fetchUsers"          ← 函数:params → {results, count}
  v-model:loading                ← 内部管理
  #filters(筛选 slot:关键字/状态/日期)
  #default(el-table 列定义)
/>
内部:「取消 + 版本号」双重防护(见 03 章)+ 分页三件套

pure 的三层表格范式同构,可作演进对照:@pureadmin/table(全局表格组件,列用 tsx cellRenderer)+ RePureTableBar(刷新/列设置拖拽排序/密度/全屏工具条,作用域插槽透传 columns)+ 业务 useUser() hook(form/dataList/pagination/CRUD 全收编)——index.vue 只剩模板解构,页面零逻辑。

4.2 表格规范

列设计   操作列 fixed right;状态列 tag 化;时间列统一格式化函数
宽表     超 6 列启用横向滚动而非挤压;关键列 min-width 而非 width
空数据   empty 占位 + 「新增第一条」引导(不是裸的「暂无数据」)
加载     表格 v-loading(不用全局遮罩)
批量     多选 + 批量操作按钮常驻 disabled(选中方可点,视觉自说明)
行操作   ≤3 个平铺;>3 个收进更多下拉
数字列   右对齐 + tabular-nums
密度     行高全站统一一档(default 48px 或 compact 40px,不混用)
图标     行内图标按钮 16px,与文字间距 6px,风格全站统一(见 06 章 4.4)

4.3 详情复用列配置(adp 思路)

ProTable 的 columns 同时喂给 ProDescriptions(详情抽屉)——列定义是数据,表格、搜索、详情三处消费同一份。Vue 端可低成本借鉴:列数组中带 formatter/render 的描述导出给详情组件。


五、交互反馈分级(全站强制)

5.1 反馈决策表

场景 反馈 理由
操作成功(删除/保存) ❌ 不弹框——界面已更新 降噪
操作失败 ✅ 错误提示(原因必带) 用户要知情
危险操作(删除/批量/不可逆) ✅ 确认框(对象名进文案) 防误操作
恢复默认/静默保存 ❌ 不弹框 降噪
按钮已禁用 ❌ 不弹框,视觉已说明 降噪
长任务(>1s) ✅ 按钮 loading;>3s 进度条 可感知
登录/MFA 失败 ✅ 表单内联错误(不 toast) 上下文相关
401 过期 ✅ 单例弹窗/单次跳转 防并发轰炸
// ❌ 冗余 —— 成功也弹
await deleteMenu(id); ElMessage.success('删除成功')
// ✅ 专业 —— 静默,界面即反馈
await deleteMenu(id); loadMenus()

5.2 三态完整性检查清单

每个数据展示组件必须显式处理三态,缺一即 bug:

加载态  skeleton(首屏)/ loading(刷新)
空态    empty + 引导动作
错误态  错误占位 + 重试按钮(不是 console.error 完事)

六、Composables / Hooks 规范

6.1 命名与分层

composables/hooks    依赖业务上下文(store/请求层):usePagedQuery、useTable
utils                纯函数无依赖:confirm、格式化函数

规则:composable 内禁止直接操作 DOM(返回响应式状态);一个 composable 只做一件事;返回值用对象而非数组(追加字段不破坏调用方)。

soybean 分层补充:框架无关的 hook(useTable 泛型四参)与 UI 适配 hook(useNaiveTable)分层——换 UI 库只动适配层。

6.2 props 与事件命名规范

  • props 用名词(fetcher、initialData、permissions),事件用动词(@close、@submit、@success)
  • 双向绑定统一 v-model:xxx 约定(如 v-model:visible),禁止拆成 visible + visibleChange 两个接口
  • 布尔 props 必须显式声明默认值;透传型组件(ListPage)用 attrs 透传 UI 库 props,并在组件头注释列出可透传项
  • 避免过度封装:props + slots 能解决的扩展,不引入 render 函数 / schema;封装层数超过两层时先考虑是否该拆组件

七、巨型单文件拆分实战

7.1 典型场景:定时任务管理页(约 1400 行单文件)

views/cron/
├── Cron.vue                # ~120 行:页面容器 + 子组件编排
├── components/
│   ├── TaskTable.vue       # 任务列表(列定义 + 批量选择)
│   ├── TaskFormDialog.vue  # 新建/编辑任务(cron 表达式编辑器独立)
│   ├── CronExprInput.vue   # cron 表达式输入 + 下次执行时间预览
│   ├── ExecutionHistory.vue# 执行记录(独立 tab/抽屉)
│   └── ExecutionLogDrawer.vue # 日志查看(自动刷新控制)
└── composables/
    └── useCronTasks.js     # CRUD 编排(复用列表 hook)

7.2 典型场景:监控大屏页(约 900 行单文件)

views/monitor/
├── Monitor.vue             # ~100 行:单机/集群切换 + 布局
├── components/
│   ├── MetricCard.vue      # 指标卡(CPU/内存/磁盘/网络 通用)
│   ├── TrendChart.vue      # 趋势图(echarts 按需引入收口在此)
│   └── HostSelector.vue    # 主机选择
└── composables/
    └── useMetricsStream.js # WebSocket 连接/重连退避/卸载断开

拆分判据:一个文件出现 ≥3 个「功能岛」(各自的数据/状态/UI 块)即拆;拆出的单元可在测试里独立渲染。组织法参照 pure 页面内聚模式(index 纯模板 + form/ 子组件 + utils/hook 收编逻辑)。


八、最佳实践清单

✅ 要做

  1. 新列表页一律「页面容器 + 列表组件」起步(模板见 05 章)
  2. 表单弹窗独立组件,页面只传数据与回调
  3. 每个数据组件三态齐全(加载/空/错误)
  4. composable 返回对象、单职责、无直接 DOM 操作
  5. 组件 props 加类型与默认值;emit 声明 events

❌ 不要

  1. 不要在页面里写第二个 <el-table>(复用列配置或拆子组件)
  2. 不要跨层 emit 链超过两级(改用 provide/inject 或提升状态)
  3. 不要在组件内散落成功类 toast(见反馈决策表)
  4. 不要用 index 作 v-for key(列表刷新错位之源)
  5. 不要复制粘贴超过两次的 UI 块不抽组件(三次法则)

九、AI 检查清单(对比目标工程逐项核对)

P0

  1. 超过 400 行的视图文件全部列出,按第七节方案给出拆分目录树
  2. 成功类 toast 违反反馈决策表的调用点列出(应删)
  3. v-for 是否有用 index 作 key 的列表(存在动态增删即不合格)

P1

  1. 列表页是否统一走列表组件协议?手写分页/裸请求的页面列出迁移清单
  2. 表单弹窗自查三件套:提交 loading、校验消息中文、关闭重置
  3. 数据组件三态是否齐全(抽检空态与错误态渲染)
  4. 权限控制是否逐按钮挂载(对照权限码清单)

P2

  1. 是否值得引入列配置「一列三态」复用(adp 思路)
  2. 弹窗是否需要升级命令式(全局单例场景用 ReDialog/ModalApi)

下一篇:08-工程化与质量保障 —— 让规范自动执行。