refactor(pc): 从 Gi Admin Pro + Arco 重搭为纯 Vue 3 + Naive UI
旧模板的 193 个文件整体删除,换成纯 Vue 3.5 + Naive UI 2.42 的树。
- 去掉 auto-import(组件与 API 全显式 import)、v-hasPerm(改 user.has('码'),全库 8 处)、
Stylelint;n-tag 的 type 统一过 utils/tag.ts 转换(Arco 色词直接透传会静默变灰)
- 图标改为构建期抽取:scripts/extract-pc-icons.py -> icons/icon-data.ts(37 个),
内网离线部署不会白屏
- 重搭后修掉 6 类"编译 / lint / typecheck 全绿、界面上就是坏的"故障:
① NGrid 的 cols 写成像素值(不是断点名 -> 永远只 1 列,工作台因此错乱)
② n-tab-pane 用 tab 属性(只渲染空注释节点,整排 tab 一个字都不显示)
③ 漏 import 被当未知自定义元素(样式整块丢) ④ .n-card__content 类名不存在
(高度链第一环失效,表格行在 DOM 里但屏幕上没有) ⑤ 填报单状态映射错位一格
⑥ 有标题的卡片没有上内边距(naive 的 padding-top 只给"内容区是长子"的卡片)
- 卡片式版面提成全局基线:styles/index.scss 的 xt-* 令牌 + .page-card / .toolbar /
.section-title,页面只引用、不再各写一套
- 品牌:改名「深学在线」,文字标换成几何标(方 = 卷宗 / 环 = 在线,不用文字字形);
index.html 的页签图标从 404 的 /favicon.ico 改指真实存在的 /favicon.svg
This commit is contained in:
@@ -1,67 +1,92 @@
|
||||
---
|
||||
name: code-vue
|
||||
description: Vue 3 Composition API、SFC 块顺序、defineEmits、useTemplateRef、响应式约定。编写或修改 .vue 文件时加载本技能。
|
||||
description: Vue 3 Composition API、SFC 块顺序、Naive UI 组件显式导入、props/emits 与模板约定。编写或修改 .vue 文件时加载本技能。
|
||||
---
|
||||
|
||||
# Vue 规范(Vue 3)
|
||||
# Vue 规范(Vue 3 + Naive UI)
|
||||
|
||||
适用于 Composition API + `<script setup lang="ts">`。样式见 [code-css](../code-css/SKILL.md)。
|
||||
|
||||
## 基础
|
||||
|
||||
- 新组件一律 **Composition API** + `<script setup>`,禁止 Options API。
|
||||
- 需要组件名时用 `defineOptions({ name: 'ComponentName' })`;路由页 `name` 与路由配置一致。
|
||||
- 块顺序:`<template>` → `<script setup>` → `<style>`(遵循项目 ESLint block-order)。
|
||||
- 新组件一律 **Composition API** + `<script setup lang="ts">`,禁止 Options API。
|
||||
- 块顺序:`<template>` → `<script setup>` → `<style scoped>`。
|
||||
- 需要组件名时用 `defineOptions({ name: 'XxxYyy' })`。
|
||||
|
||||
## Script 内顺序
|
||||
|
||||
类型导入 → 第三方 → Vue API → 项目模块 → `defineOptions` → `defineModel` → `defineProps` + `withDefaults` → `defineEmits` → `defineSlots` → 常量/函数 → `ref`/`reactive` → `computed` → 方法 → 生命周期/`watch` → `defineExpose`
|
||||
类型导入 → 第三方 → Vue API → 项目模块 → `defineOptions` → `defineProps` + `withDefaults` → `defineEmits` → 常量/函数 → `ref`/`reactive` → `computed` → 方法 → 生命周期/`watch`。
|
||||
|
||||
## 自动导入
|
||||
## ⚠️ 显式 import(本仓库没有 auto-import)
|
||||
|
||||
已自动注册的 API / 组件不要重复 import;仅需类型时用 `import type`。以 `vite.config` 与现有代码为准。
|
||||
```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)` 处理;插槽那条分支返回数组,所以正常。
|
||||
|
||||
## 权限与状态色
|
||||
|
||||
```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**。
|
||||
- 受控表单优先 `v-model`;复杂逻辑抽到 `computed` 或方法,不要写在模板里。
|
||||
- 子组件通过 props/emits 通信,避免随意访问父组件实例。
|
||||
|
||||
## defineEmits
|
||||
|
||||
使用**基于类型的函数调用签名**,禁止数组 / tuple 形式:
|
||||
|
||||
```typescript
|
||||
// ✅
|
||||
const emit = defineEmits<{
|
||||
(e: 'submit'): void
|
||||
(e: 'change', value: string): void
|
||||
}>()
|
||||
|
||||
// ❌ defineEmits<{ submit: [] }>() 或 tuple 形式
|
||||
```
|
||||
|
||||
## 模板 Ref
|
||||
|
||||
优先 **`useTemplateRef`**(Vue 3.5+);模板 `ref` 属性值与参数字符串一致。
|
||||
|
||||
```typescript
|
||||
const formRef = useTemplateRef<FormInstance>('formRef')
|
||||
// template: <form ref="formRef">
|
||||
```
|
||||
|
||||
## 响应式
|
||||
|
||||
- 原始值 `ref`,对象 `reactive`,派生 `computed`。
|
||||
- 解构 reactive 注意响应性丢失,必要时 `toRefs`。
|
||||
- 深拷贝优先 `structuredClone` 或项目工具,禁止 `JSON.parse(JSON.stringify())`。
|
||||
|
||||
## defineExpose
|
||||
|
||||
仅暴露父组件确实需要调用的方法或属性(如弹窗 `open`/`close`/`reset`)。
|
||||
|
||||
## 禁止
|
||||
|
||||
- ❌ Options API、重复自动导入、defineEmits tuple 形式
|
||||
- ❌ 无关 `ref()` 绑定模板 ref(优先 `useTemplateRef`)
|
||||
- ❌ 模板中写复杂业务逻辑
|
||||
- 表格列用 `h()` 渲染(`NButton` / `NTag` / `AppIcon`),逻辑别堆在模板里。
|
||||
- 受控表单优先 `v-model`;复杂逻辑抽到 `computed` 或方法。
|
||||
- 图标统一用 `@/icons/AppIcon.vue`(缺图标是**空占位、不报错**)。
|
||||
- 子组件通过 props/emits 通信,避免访问父组件实例。
|
||||
|
||||
Reference in New Issue
Block a user