基于 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 加密前缀保留(已加密凭证不受影响)。
254 lines
12 KiB
Markdown
254 lines
12 KiB
Markdown
# 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) —— 让规范自动执行。
|