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

254 lines
12 KiB
Markdown
Raw Permalink 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.
# 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,声明式首选)
```tsx
// 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,复杂弹层)
```ts
// 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 过期 | ✅ 单例弹窗/单次跳转 | 防并发轰炸 |
```js
// ❌ 冗余 —— 成功也弹
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**
4. 列表页是否统一走列表组件协议?手写分页/裸请求的页面列出迁移清单
5. 表单弹窗自查三件套:提交 loading、校验消息中文、关闭重置
6. 数据组件三态是否齐全(抽检空态与错误态渲染)
7. 权限控制是否逐按钮挂载(对照权限码清单)
**P2**
8. 是否值得引入列配置「一列三态」复用(adp 思路)
9. 弹窗是否需要升级命令式(全局单例场景用 ReDialog/ModalApi)
---
> 下一篇:[08-工程化与质量保障](08-工程化与质量保障.md) —— 让规范自动执行。