基于 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 加密前缀保留(已加密凭证不受影响)。
12 KiB
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 即校验源) | 中 |
表单通用规则(必须):
- 提交按钮 loading 态(防双击重复提交)
- 校验消息中文、具体(「请输入用户名」而非「username 不能为空」)
- 编辑态回填后 reset 时机明确(打开时回填,关闭时重置)
- 危险输入(密码)用
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 收编逻辑)。
八、最佳实践清单
✅ 要做
- 新列表页一律「页面容器 + 列表组件」起步(模板见 05 章)
- 表单弹窗独立组件,页面只传数据与回调
- 每个数据组件三态齐全(加载/空/错误)
- composable 返回对象、单职责、无直接 DOM 操作
- 组件 props 加类型与默认值;emit 声明 events
❌ 不要
- 不要在页面里写第二个
<el-table>(复用列配置或拆子组件) - 不要跨层 emit 链超过两级(改用 provide/inject 或提升状态)
- 不要在组件内散落成功类 toast(见反馈决策表)
- 不要用 index 作 v-for key(列表刷新错位之源)
- 不要复制粘贴超过两次的 UI 块不抽组件(三次法则)
九、AI 检查清单(对比目标工程逐项核对)
P0
- 超过 400 行的视图文件全部列出,按第七节方案给出拆分目录树
- 成功类 toast 违反反馈决策表的调用点列出(应删)
- v-for 是否有用 index 作 key 的列表(存在动态增删即不合格)
P1
- 列表页是否统一走列表组件协议?手写分页/裸请求的页面列出迁移清单
- 表单弹窗自查三件套:提交 loading、校验消息中文、关闭重置
- 数据组件三态是否齐全(抽检空态与错误态渲染)
- 权限控制是否逐按钮挂载(对照权限码清单)
P2
- 是否值得引入列配置「一列三态」复用(adp 思路)
- 弹窗是否需要升级命令式(全局单例场景用 ReDialog/ModalApi)
下一篇:08-工程化与质量保障 —— 让规范自动执行。