CRMEB/frontend/admin/docs/vue3-migration/page-conversion-guide.md
2026-09-23 15:15:02 +08:00

98 lines
7.8 KiB
Markdown
Raw 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.

# Vue3 迁移:单页/组件转换规范(所有批次必须遵守)
本仓库正在从 Vue2.7 迁移到 Vue3 + `<script setup>`。codemod 已批量处理过模板语法(slot/.sync/.native/deep/$set/require 资产/生命周期名),你负责把指定范围的 `.vue` 文件转为 Vue3 script setup 并修复残留问题。**禁止 TypeScript。保留文件顶部 CRMEB 版权注释。**
先读这三份资料再动手:
1. `/Users/from/.zcode/skills/vue2-to-vue3-migration/references/migration-patterns.md`(API 映射表)
2. `docs/vue3-migration/store-mapping.md`(Vuex→Pinia 映射,本项目 store API 名单)
3. 本文件
## 转换规则
### script 结构
- `<script>` → `<script setup>`;`name` → `defineOptions({ name: 'Xxx' })`(keep-alive 依赖组件名,**必须保留原 name**)。
- `data` → `ref()/reactive()`;`methods` → 普通 function;`computed` → `computed()`;`watch` → `watch()`(监听 props 必须用 getter:`watch(() => props.x, ...)`)。
- 生命周期:`created` → 顶层代码;`mounted` → `onMounted`;`beforeUnmount/unmounted`(codemod 已改名)→ `onBeforeUnmount/onUnmounted`;`activated/deactivated` → `onActivated/onDeactivated`。
- `this.$refs.x` → `const xRef = ref(null)` + 模板 `ref="xRef"`(**模板 ref 名必须与变量名一致**,原有模板 `ref="x"` 时把变量命名为 `x` 对应 `const x = ref(null)` 最省事——但注意与 data 变量重名冲突)。
- `this.$emit` → `defineEmits`;`this.$nextTick` → `nextTick`;`this.$set/$delete` 已被 codemod 处理(残留多为注释)。
- `this.$route`/`this.$router` → `useRoute()`/`useRouter()`。
- `props` → `defineProps`;`components: {...}` 局部注册删除(直接 import 即可用);异步组件 `() => import('...')` → `defineAsyncComponent`。
- 组件内 `filters: {...}` 选项:转为普通 function(codemod 已把模板改成 `fname(expr)` 直接调用)。
- `mixins: [...]`:内联展开为组合逻辑;`provide/inject`(选项式)→ `provide()`/`inject()`。
- `<script setup>` 顶层 import 的组件名必须与模板标签一致(如模板用 `<draggable>` 就必须 `import draggable from 'vuedraggable'`)。
- watch `$route` → `watch(() => route.path, ...)`。
### store(Vuex → Pinia)
- `this.$store.state.xxx.yyy` → `const xxxStore = useXxxStore()` 后 `xxxStore.yyy`(解构用 `storeToRefs`)。
- `this.$store.commit('menu/setActivePath', p)` → `useMenuStore().setActivePath(p)`;`dispatch` 同理。
- `mapState/mapMutations/mapActions` 删除,改直接调用;函数名以 `docs/vue3-migration/store-mapping.md` 为准(注意 userInfo store 的 setter 都加了 `set` 前缀、menus 的 `setChildMenuList`)。
- 非命名空间模块(user/app)的历史裸调用 `commit('setUserId')` → `useUserStore().setUserId()`。
### 事件总线
- `this.bus.$on('e', fn)` → `import bus from '@/utils/bus'` + `bus.on('e', fn)`;`$emit` → `bus.emit`;`$off` → `bus.off`(**必须具名函数**,不能内联箭头)。
- wangEditor 目录里的 `import bus from '@/utils/bus'`(原来是 new Vue() 实例)调用点 `$on/$emit` → `on/emit`。
### 全局属性替代(script setup 无 this)
| 旧写法 | 新写法 |
|---|---|
| `this.$message.success(...)` | `import { ElMessage } from 'element-plus'` + `ElMessage.success(...)` |
| `this.$confirm/$alert/$prompt/$msgbox/$notify` | `ElMessageBox.confirm/alert/prompt` / `ElMessageBox(...)` / `ElNotification`(from 'element-plus') |
| `this.$modalForm(...)` | `import modalForm from '@/utils/modalForm'` 直接调用 |
| `this.$modalSure(...)` | `import { modalSure } from '@/utils/public'` |
| `this.$HandlePrice` / `$exportExcel` / `$videoCloud` / `$authLapse` | `@/utils/public` / `@/utils/newToExcel.js` / `@/utils/videoCloud` / `@/utils/authLapse` 直接 import |
| `this.$wechat` | `import Auth from '@/libs/wechat'` |
| `this.$dialog.error(...)` | `import dialog from '@/libs/dialog'` |
| `this.$moment(...)` | `import moment from 'moment'` |
| `this.$timeOptions` / `$scroll` | `@/libs/timeOptions` / `@/libs/loading` |
| `this.$tools` | `import * as tools from '@/libs/tools'` |
| `this.$routeProStr` | `import Setting from '@/setting'` 后 `Setting.routePre` |
| `this.$config` | `import config from '@/config'` |
| `this.$validator(rule)` | `import schema from 'async-validator'` + `new schema(rule)` |
| `this.$imgModal(...)` / `this.$videoModal(...)` | `import { getCurrentInstance } from 'vue'` 后 `const { proxy } = getCurrentInstance()` + `proxy.$imgModal(...)`(这两个保持 proxy 调用) |
| `this.$copyText(text)` | 同上走 proxy,或复制逻辑内联 |
### Element Plus 差异(codemod 未覆盖、需要你手工处理)
- `<el-dialog>` 插槽 `#title` → `#header`;`el-drawer` 的 `#title` 不变。
- `el-submenu` 已改 `el-sub-menu`(其 `#title` 插槽保留)。
- size:`medium` → `default`、`mini` → `small`(属性值是字符串的场合)。
- `el-button icon="el-icon-xxx"` 字符串类名**保留不动**(已有 CSS mask 兼容层);`<i class="el-icon-xxx">` 同样保留。
- `el-select` 等 `:visible.sync` 已被 codemod 处理;若发现组件自定义 `visible` prop 事件为 `update:visible` 逻辑保持。
- `el-table` 作用域插槽已是 `#default="scope"`。
- `el-tooltip`/`el-popover` 的 `slot="content"` 已被 codemod 包成 `<template #content>`。
- `el-form` 的 `:model` 校验、`ref` 调用 `validate` 用法不变;`resetFields` 不变。
- 若遇到 `el-autocomplete` `popper-class` 等样式类在 theme 里未对齐,先不改样式。
### 其他
- 模板里残留的 `require('@/assets/...')`(codemod 已报告,见 scripts/codemods/report.json 的 tpl-require):改为顶部 `import` + 标识符。
- `v-for` 与 `v-if` 同标签(report.json 有清单):拆成 `<template v-for>` 内套 `v-if`(注意 Vue3 语义:v-if 优先级更高,先确认原逻辑等价于"对每项判断"还是"整体判断")。
- `<template v-for>` 的 `:key` 必须放在 `<template>` 上(Vue3 强制)。
- `v-model` 不能绑 v-for 循环变量本身,用 `v-model="array[index].field"`。
- 图片懒加载 `v-lazy="url"` **保留**(已有兼容指令,只支持 img src 场景)。
- `v-viewer`、`v-db-click`、`v-auth`、`v-permission`、`v-clipboard`、`v-draggable` 保留。
- 遇到 `swiper/swiper-slide`、`vuedraggable`、`vue-codemirror`、`vxe-table` 等第三方组件用法:本批只保证语法/编译通过,把 import 改为可解析(swiper11: `import { Swiper, SwiperSlide } from 'swiper/vue'` + `import 'swiper/css'`;vuedraggable4: `import draggable from 'vuedraggable'`,模板需加 `:item-key` 并把循环项包进 `#item="{ element, index }"` 插槽——若工作量大可先保留旧模板结构并在文件顶部加 `// TODO(vue3): draggable API 迁移` 注释,保证编译通过即可)。
- scss 里的 `::v-deep` 已处理;`/deep/` 若残留改 `:deep()`。
## 验证方式(必须执行)
dev server 已在 http://localhost:1619/admin 常驻运行。逐个请求你改过的文件(编译通过=200,失败=500):
```bash
curl -s -o /dev/null -w '%{http_code} src/xxx.vue\n' 'http://localhost:1619/admin/src/xxx.vue'
```
- 批量脚本示例:`for f in $(git diff --name-only -- 'src/components' | sed 's|^src/|src/|'); do curl -s -o /dev/null -w "%{http_code} $f\n" "http://localhost:1619/admin/$f"; done | grep -v '^200'`
- 非 200 的必须修到 200(500 时 curl 响应体里有 vite 的报错详情,`curl -s 'http://...' | head -30` 查看)。
- 额外用 `npx prettier --check <files>` 确认无语法错误。
- **不要**自己启动/杀掉 dev server;**不要**跑 npm install。
## 提交
完成后:`git add <你负责的目录们> && git commit -m "<给定信息>"`。若 git 报 index.lock 冲突,sleep 5 重试。不要 add 别的目录,不要 push。