624a05aa66
需求:「设计开发平台运营人员对租户的管理功能,还有日志的详情查看功能。」 ⚠️ **口径变更**(覆盖 2026-10 走查的旧结论):views/tenant/index.vue 原先写着「列表**没有操作列**: 不提供编辑 / 停用入口(客户 2026-10 走查确认的口径)」。本轮用户明确选了全量范围,所以列表现在 有操作列(详情 / 编辑 / 停用·启用)。旧注释是**改写而不是删掉**,并写明推翻理由 —— 运营的真实 诉求是:① 到期日变了要能自己改,而不是每次找开发;② 欠费 / 停止合作时要能当场停掉; ③ 每周核对"谁快到期了"时要能点进详情看用量。 1) 租户管理:操作列用 tenant:view 门控「详情」、tenant:manage 门控「编辑 / 停用 / 启用」, 拿不到管理权限时整列显示"只读",而不是留一个点了报 403 的按钮。停用走 NPopconfirm 二次确认 且文案写明后果("停用后该校所有学生与老师会立刻无法登录");只发 {status} 一个字段 —— PUT 是部分更新,回传整行会把列表里看不见的字段覆盖成空。 2) 详情抽屉 TenantDetailDrawer:打开时**同时**调 getTenantDetail 与 getTenantUsage —— 列表接口的 contactPhone **恒为 null**(电话以密文入库,列表不能成为批量拉取联系方式的通道), 脱敏值只有详情接口给,拿行数据凑详情会显示成"没有联系电话"。用量区写明"学生数与存储是**实时值**, 周期只影响计费项",否则会以为"2026Q3 的学生数"是历史值;notes 如实全列(它解释"为什么这几项是 0")。 统计周期只给本月 / 上月 / 本季三个固定选项,不做自由文本输入。 3) 编辑弹窗 TenantEditModal:类型 4 选项、状态 3 选项;数字下拉的校验规则必须带 type: 'number' (只写 required 会"看起来填了但校验不过",而且 #feedback 会把报错遮住);联系电话留空 = 不修改; 租户编码只读展示并注明"出现在扫码落地页与对接文档里,开通后不可修改"。不做"清空到期日" (MyBatis-Plus 忽略 null 更新,清不掉就不在界面上承诺)。 4) 日志页:操作日志 / 登录日志两个模式,各自维护 list / total / pageNum / pageSize / loading / 筛选条件,切换时重置到第 1 页。⚠️ 页签用工具条上的**分段按钮**而不是 NTabs / NTabPane: ① NTabPane 的内容要额外 flex 化才能维持 .page-card 的高度链,否则表格高度算成 0、 **一行数据都看不见**(本仓踩过);② naive 2.45.3 的 tab="..." 属性写法本身也是坏的, 必须用 #tab 插槽。 5) 日志详情 LogDetailDrawer:变更前后**完整不截断** + break-all + 各一个复制按钮; extraJson 格式化成 <pre>(解析失败原样展示);traceId 带复制;request_param / duration_ms 如实写"本版本未采集"(这两列从来没有写入点),不编。抽屉**不再打接口** —— 列表 SELECT 已经 把抽屉要的字段都带出来了。 6) 「单位」列只对平台显示(0 → 「平台」),两张表统一用「单位」这个词:这一列要装得下 tenant_id = 0 的「平台」,表头写「租户」而格子里写「平台」是自相矛盾的;且与筛选栏两个下拉的 占位符、租户管理页的「单位名称」一致。登录日志刻意**不要「归属地」列**(location 恒为 null, 整列空白看起来像功能坏了)。 7) 日志页的模块下拉换成导出的 MODULE_OPTIONS / TARGET_TYPE_OPTIONS:原先那 4 项里 AUDIT / EXPORT **后端一个写入点都没有**,而真会写的 FILE(代传材料)与 ROLE(6 个角色动作)**不在里面** —— 选中死项只会得到一张空表,让人以为"这段时间没有审核动作"。 8) 菜单显示名「操作日志」→「日志」(key 保持 'log' 不动:后端 BuiltinRole 的菜单键与 check-wiring 第 ⑥ 项都按它核对,改 key 会让平台角色的菜单静默少一项)。 同步 frontend-pc/AGENTS.md 与 .agents/:「最容易写错的约定」由 7 条扩到 **9 条** —— 新增 第 8 条「详情用抽屉、不要为详情新开路由」(含"列表接口可能刻意不返回某个字段"的坑)与 第 9 条「页面内的页签用工具条分段按钮」(写明高度链的原因)。 验证:pnpm typecheck **0 错**、pnpm lint **0 错 0 警告**、pnpm build 通过。 浏览器实测(平台账号,5173 + 8080):租户页出现操作列且三档到期配色正确、卡片上内边距仍是 16px、 高度链未破(表格 570px / 日志表 528px);详情抽屉显示脱敏电话 138****8000 —— 而库里存的是 **历史明文** 13800138000,也就是说这条同时验了"读路径容错"(否则是永久 500);停用二次确认文案 正确;清空单位名称提交后弹窗不关且错误可见;登录日志同一张表里同时出现「示范职业学校」 (tenant_id=1)与「平台」(tenant_id=0)两种映射;**学校管理员视角两张表都没有「单位」列**。
116 lines
5.1 KiB
Markdown
116 lines
5.1 KiB
Markdown
---
|
||
name: code-vue
|
||
description: Vue 3 Composition API、SFC 块顺序、Naive UI 组件显式导入、props/emits 与模板约定。编写或修改 .vue 文件时加载本技能。
|
||
---
|
||
|
||
# Vue 规范(Vue 3 + Naive UI)
|
||
|
||
适用于 Composition API + `<script setup lang="ts">`。样式见 [code-css](../code-css/SKILL.md)。
|
||
|
||
## 基础
|
||
|
||
- 新组件一律 **Composition API** + `<script setup lang="ts">`,禁止 Options API。
|
||
- 块顺序:`<template>` → `<script setup>` → `<style scoped>`。
|
||
- 需要组件名时用 `defineOptions({ name: 'XxxYyy' })`。
|
||
|
||
## Script 内顺序
|
||
|
||
类型导入 → 第三方 → Vue API → 项目模块 → `defineOptions` → `defineProps` + `withDefaults` → `defineEmits` → 常量/函数 → `ref`/`reactive` → `computed` → 方法 → 生命周期/`watch`。
|
||
|
||
## ⚠️ 显式 import(本仓库没有 auto-import)
|
||
|
||
```ts
|
||
import type { DataTableColumns, FormInst } from 'naive-ui'
|
||
import { NButton, NCard, NDataTable, NForm, NFormItem, NInput, NModal, NTag, useMessage } from 'naive-ui'
|
||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||
import * as tenantApi from '@/apis/xt/tenant'
|
||
```
|
||
|
||
- 组件用 **PascalCase** 标签:`<NDataTable :columns="columns" :data="list" />`(**不是** `<a-table>`)。
|
||
- 弹窗/抽屉、表格、表单全部来自 **naive-ui**;主题与全局配置在 `utils/naive.ts` / `App.vue` 一侧。
|
||
|
||
### 漏 import 是**静默故障**(2026-10 实测踩到 4 个文件 6 处)
|
||
|
||
写小写标签(`<n-button>`)而忘了 import 时,Vue 把它当**未知自定义元素**渲染:
|
||
**不报错、不崩、`pnpm typecheck` 与 `pnpm lint` 全绿**,只是样式整块丢掉。
|
||
实测表现:`<n-button>` 变成无边框透明块、`<n-avatar>` 变成 18×29 的裸元素、`<n-form>` 的表单排版全丢 ——
|
||
用户看到的就是"这个页面样式坏了"。
|
||
|
||
> **统一写 PascalCase 标签**(`<NButton>`),漏 import 时 Vue 会给出
|
||
> `Failed to resolve component` 警告,比小写标签好排查。
|
||
> 另外 `python scripts/check-wiring.py` 的**第 ⑧ 项**会静态核对
|
||
> 「模板里用到的 `<n-*>` ↔ 本文件的 import」,改完 PC 端跑一下它。
|
||
|
||
## ⚠️ `n-tab-pane` 的标签必须用 `#tab` **插槽**
|
||
|
||
naive-ui 2.45.3 里,`tab="文字"` **属性**写法渲染不出任何文字(只留一个空注释节点),
|
||
表现是**整排 tab 一个字都不显示、整行高度只剩 12px** —— 筛选功能等于消失,且不报错。
|
||
|
||
```vue
|
||
<!-- ✅ 正确 -->
|
||
<NTabs v-model:value="node">
|
||
<NTabPane :name="1">
|
||
<template #tab>
|
||
初审(班主任)
|
||
</template>
|
||
</NTabPane>
|
||
</NTabs>
|
||
|
||
<!-- ❌ 不要:标签会静默消失 -->
|
||
<NTabPane :name="1" tab="初审(班主任)" />
|
||
```
|
||
|
||
原因:属性值走 `normalizeVNode(() => render(mergedTab ?? name))`,而这个 thunk 返回
|
||
Text VNode,被当成块 `createBlock(vnode)` 处理;插槽那条分支返回数组,所以正常。
|
||
|
||
### 页面内的「页签」优先用工具条上的分段按钮
|
||
|
||
如果只是在一屏里切换两张表(例:日志页的「操作日志 / 登录日志」),
|
||
**用两个 `NButton` 做分段控件,不要用 `NTabs`**:
|
||
|
||
- `NTabPane` 的内容要额外 flex 化才能维持 `.page-card` 的高度链,
|
||
否则表格高度算成 0、**一行数据都看不见**(本仓踩过这个坑);
|
||
- 上面那个 `tab="..."` 的属性坑也在同一条路上,能绕开就绕开。
|
||
|
||
两个按钮在视觉上就是页签,而且不动高度链。各页签的 `list` / `total` / `pageNum` /
|
||
`loading` / 筛选条件要**分开维护**,切换时重置到第 1 页。
|
||
|
||
## 详情用抽屉,不要为详情新开路由
|
||
|
||
`NDrawer` + `NDrawerContent` + `NDescriptions` / `NDescriptionsItem`,
|
||
模板抄 `src/views/audit/AuditDetailDrawer.vue`。列表里的入口是**操作列**里的一个
|
||
`text` 按钮(`size="small"`)。
|
||
|
||
⚠️ 抽屉打开时该调哪个接口要看清楚:**列表接口可能刻意不返回某个字段**。
|
||
例:`/api/v1/tenants` 的 `contactPhone` **恒为 `null`**(联系电话以密文入库,
|
||
列表不能成为批量拉取联系方式的通道),脱敏值只有 `/api/v1/tenants/{id}` 给 ——
|
||
拿行数据凑详情,界面上会显示成"没有联系电话",而这种错看起来像数据问题,不像代码问题。
|
||
|
||
## 权限与状态色
|
||
|
||
```ts
|
||
const user = useUserStore()
|
||
// 没有 v-hasPerm 指令,权限判断就是一次函数调用
|
||
const canManage = computed(() => user.has('org:manage'))
|
||
```
|
||
|
||
```vue
|
||
<NTag
|
||
:type="tagOf(row.status)"
|
||
size="small"
|
||
>
|
||
{{ STATUS_NAMES[row.status] }}
|
||
</NTag>
|
||
```
|
||
|
||
⚠️ **不要**把 `apis/xt/*.ts` 里的 `color`(Arco 色词 `green`/`arcoblue`/`gold`)直接传给 `n-tag` 的 `type` ——
|
||
会**静默变成默认灰色**且不报错;必须过 `@/utils/tag.ts` 的 `tagOf()`。
|
||
|
||
## 模板
|
||
|
||
- 事件名、插槽名 **kebab-case**;组件 **PascalCase**。
|
||
- 表格列用 `h()` 渲染(`NButton` / `NTag` / `AppIcon`),逻辑别堆在模板里。
|
||
- 受控表单优先 `v-model`;复杂逻辑抽到 `computed` 或方法。
|
||
- 图标统一用 `@/icons/AppIcon.vue`(缺图标是**空占位、不报错**)。
|
||
- 子组件通过 props/emits 通信,避免访问父组件实例。
|