Back to skills
extension
Category: Development & EngineeringNo API key required

vue2-vue3-element-plus-migration

一个自包含的通用手册,帮助开发者把 Vue 2 + Element UI(webpack/Vue CLI)项目迁移到 Vue 3 + Vite + Element Plus。内置:① 先扫后改的 grep 检测命令(标签 + PascalCase 双扫,防漏死 import);② P1–P17 高频坑点修复写法,覆盖自定义 select 包装组件传参 bug、el-dialog v-model 受控、el-table 选区 API、el-date-picker Day.js 格式、vue-router 重名、vue-clipboard3 指令、require ESM 雷、this. 模板白屏、mini size 等;③ 迁移后验证流程(控制台零报错 / 弹框关闭 / 下拉宽度与传参 / 表格日期);④ 症状→原因→修复速查表。适用于现代化迁移、升级排错与已迁移代码审计。

personAuthor: gentlydinghubModelScope

Vue 2 → Vue 3 + Element UI → Element Plus 迁移手册(通用版)

一个自包含、可独立分享的迁移手册。覆盖从 Vue 2 + Element UI(webpack / Vue CLI)升级到 Vue 3 + Vite + Element Plus 的完整模式、检测命令、修复写法和验证流程。适用于:

  • 把老 Vue 2 项目现代化迁移到 Vue 3;
  • 把 Element UI 升级为 Element Plus;
  • 迁移后排查运行时问题(白屏、el-dialog 取消/提交不关闭、下拉框宽度异常或向后端传参错误、控制台报错);
  • 对已迁移代码做"坑点审计"。

本手册不依赖任何其它技能文件,可直接复制、提交到 git 或上架技能市场。


1. 推荐迁移顺序(先扫后改,分批进行)

  1. 先全量扫描:用第 2 节的 grep 命令列出所有命中文件,建立"待改清单"。
  2. 先修会崩/白屏的硬伤(slot-scope、this.<style> 注释、$vnode、重复路由 namerequire 雷),这些会直接阻断渲染。
  3. 再修组件 API 与数据绑定(el-dialog v-model、el-table 选区、el-date-picker 格式、自定义 select 包装组件传参)。
  4. 最后做验证(控制台报错 + 弹框关闭 + 下拉宽度与传参)。

不要一次全改完再验证——分批改、每批抽几个页面本地 npm run dev 验证。


2. 检测:先用 grep 扫描整个代码库

下面这些命令能快速定位绝大多数坑(在 src/ 根目录执行):

# —— 组件语法 / 模板 ——
grep -rn "slot-scope" src/                                  # 旧 scoped slot 语法(必崩)
grep -rn "scope=\"scope\"" src/                             # <template scope="scope"> 旧语法
grep -rn "visible.sync" src/                               # .sync 修饰符(已废弃)
grep -rn "v-bind\$listeners\|\$listeners" src/             # $listeners 已移除
grep -rn "\$vnode" src/                                    # $vnode 已移除
grep -rn "this\." src/ --include=*.vue | grep "<template"  # 模板里写 this.(白屏)

# —— Element Plus 重命名 ——
grep -rn "el-dialog|el-table|el-date-picker|el-select" src/ # 大体量组件,逐个核对 API
grep -rn "size=\"mini\"" src/                              # Element Plus 无 mini size
grep -rn "getSelectionRows\|\.selection" src/              # el-table 选区 API
grep -rn "value-format\|format=" src/ | grep "el-date-picker"  # Day.js 大写

# —— 自定义 select 包装组件(传参大坑)——
grep -rn "organization-select|emp-select|product-supplier-select" src/
grep -rn "OrganizationSelect|EmpSelect|ProductSupplierSelect" src/

# —— 路由 / 指令 / 模块系统 ——
grep -rn "name:" src/router*                               # 同名 name 静默覆盖
grep -rn "vue-clipboard3\|v-clipboard" src/                # clipboard 指令需重注册
grep -rn "require(" src/                                   # Vite ESM 无 CommonJS

# —— echarts 在弹框/悬停框内(初始化时序 + 裁剪)——
grep -rn "echarts.init\|\$echarts.init" src/ | grep -i "popover\|tooltip\|dialog\|show"  # 弹框内图表
grep -rn "el-popover" src/ | grep -i "chart\|echarts"      # 悬停弹框嵌图表

# —— 样式 ——
grep -rn "//" src/ --include=*.vue | grep "<style"         # <style> 无 lang 禁 // 注释

# —— echarts 实例放 data()(响应式劫持,致命)——
grep -rn "echarts.init\|\$echarts.init" src/              # 找全部图表初始化点
grep -rn "data()" src/ | grep -i "chart\|echarts"          # 实例是否声明在 data() 里

# —— el-dialog 与内部 el-table 同名 ref ——
# 人工核对:同一个 ref 名是否同时用在了 el-dialog 与它的子 el-table 上

# —— this.$set 已移除(Vue 3 无此方法)——
grep -rn "this.\$set\|\$set(" src/                          # Vue3 直接赋值即可

# —— 自定义弹窗:裸 v-model vs 命名 v-model:visible ——
grep -rn "v-model:visible\|update:visible" src/            # 子组件用 visible+update:visible 时
grep -rn "<xxx-dialog v-model=" src/ | grep -i "dialog"    # 父级是否误用裸 v-model 传 visible

提示:上面若干检测需结合人工核对(如 ref 同名、v-model 模式),grep 仅给出候选。

⚠️ kebab-case 标签 emp-select 与 PascalCase EmpSelect(import/注册名)不会同时命中,扫完标签后务必再扫一遍 PascalCase,避免漏掉"死 import"。


3. 模式目录(含检测 + 修复)

P1. <template scope="scope"> 旧语法 → 具名插槽

  • 问题:Vue 3 中 <template scope="scope"> 直接崩。
  • 修复:改为 <template #default="scope">(或 <template v-slot:default="scope">)。
  • 注意同时把 slot="xxx" 属性改为 #xxx 具名插槽。

P2. .sync 修饰符 → v-model:prop / update:prop

  • 问题:Vue 3 移除 .sync
  • 修复:visible.sync="x"v-model:visible="x";子组件 this.$emit('update:visible', v) 不变。

P3. slot-scopev-slot#

  • 问题slot-scope 已废弃。
  • 修复slot="header" slot-scope="props"<template #header="props">

P4. $listeners 已移除 → 用 v-bind="$attrs" + inheritAttrs: false

  • 问题this.$listeners 在 Vue 3 不存在,组件透传事件失效。
  • 修复:父组件透传用 v-bind="$attrs";子组件 inheritAttrs: false 后手动绑定到根元素。

P5. $vnode 已移除

  • 问题this.$vnodeundefined,访问即报错。
  • 修复:用 this.$.vnodethis.$options.name 替代(后者拿组件名最稳)。

P6. 模板里禁写 this.(白屏)

  • 问题:Vue 3 模板作用域不再暴露 this<template> 里写 this.xxx 会白屏/报错。
  • 修复:模板里直接写 xxx(数据、计算属性、方法均可直接引用)。this. 只在 <script> 方法体内保留。

P7. <style>lang 时禁止 // 单行注释

  • 问题:纯 CSS 不支持 //,PostCSS 报 Unexpected '/'、编译失败。
  • 修复:用 /* */ 块注释;或给 <style>lang="scss"

P8. Element Plus 无 mini size → small

  • 问题size="mini" 在 Element Plus 无效,回退为默认 default,布局变松散。
  • 修复:统一改为 size="small"(或保留 default 并调整间距)。全局可在 app.use(ElementPlus, { size: 'small' }) 设置。

P9. el-dialogv-model 受控约定(弹框不关闭的根因)

  • 问题:父组件 <PlanDialog v-model="visible" /> 实际传 modelValue prop 并监听 update:modelValue。若子组件自己定义 visible prop + 本地 dialogVisible 镜像 + emit('update:visible'),与父级 v-model 不匹配,导致"取消/提交"关闭事件传不回父级。
  • 修复:子组件直接受控于 modelValue
    <!-- 子组件模板 -->
    <el-dialog :model-value="modelValue" @update:model-value="$emit('update:modelValue', $event)">
    <!-- script -->
    props: { modelValue: { type: Boolean, default: false } }
    methods: { handleClose() { this.$emit('update:modelValue', false) } }
    
  • 若弹框是页面级(父组件就是 el-dialog 本身),确认 v-model="dialogVisible" 绑的是响应式数据,关闭逻辑 this.dialogVisible = false@close 正常。

P10. el-table 选区 API 变更

  • 问题this.$refs.table.selection 在 Element Plus 可能为空或不稳定。
  • 修复:用 this.$refs.table.getSelectionRows() 获取选中行(返回数组)。

P11. el-date-pickervalue-format 用 Day.js 大写 YYYY-MM-DD

  • 问题:Element Plus 用 Day.js,value-format/format 必须是大写 YYYY-MM-DD HH:mm:ss(小写 y/d 输出乱码、无法选日期、传参错)。
  • 坑中坑:如果项目里有自定义的 Date.prototype.format 方法(如 new Date().format('yyyy-MM-dd')),那是另一套只认小写的实现,不要把它的调用改成大写——两者互不影响,按各自 API 来。

P12. 自定义 el-select 包装组件的传参 bug(高频大坑)★★★★★

  • 背景:很多项目自己包了一层 <organization-select> / <emp-select> / <product-supplier-select>(内部用 el-select + 调接口加载选项,emit 整个 option 对象)。
  • 典型缺陷
    • 内部 value 数据、emit 整个 {value, label} 或 emp 对象;
    • Emp 组件 option 用 :value="item.id",但父级 @change 里写 obj.value → 取到的 undefined,向后端传参静默成空(这是最常见的"查不出数据"根因);
    • 组件内 el-selectwidth:100%,在 inline 表单里没有显式宽度,导致宽度塌陷。
  • 推荐修复(彻底去包装):替换为原生 el-select + 本地数据数组,用 v-model 直接绑字段。
    • 数据 API 映射(按项目实际路径调整):
      • 组织:organizationsSelectData(@/api/hr/organization/organization)res 为数组,元素 {value, label}
      • 员工:empsPage(@/api/mdm/emp/emp)res.data 为数组,元素含 idproductType.explanationproductDescription
      • 供应商:productSuppliersSelectData(@/api/mdm/product-supplier/product-supplier)res 为数组,元素 {value, label}
    • 模板写法:
      <!-- organization / product-supplier -->
      <el-select v-model="form.orgCode" :clearable="true" filterable placeholder="请选择" style="width:200px">
        <el-option v-for="item in organizations" :key="item.value" :label="item.label" :value="item.value" />
      </el-select>
      <!-- emp:注意 value 用 item.id -->
      <el-select v-model="form.empId" :clearable="true" filterable placeholder="请选择" style="width:200px">
        <el-option v-for="item in emps" :key="item.id"
          :label="'('+item.productType.explanation+')'+item.productDescription" :value="item.id" />
      </el-select>
      
    • script:
      • 删掉对 OrganizationSelect/EmpSelect/ProductSupplierSelect 的 import 和 components 注册;
      • dataorganizations: [], emps: [], productSuppliers: []
      • created() 加载:organizationsSelectData().then(r => this.organizations = r || [])empsPage({}).then(r => this.emps = (r && r.data) || [])productSuppliersSelectData().then(r => this.productSuppliers = r || [])
      • 绑定字段必须与原 @change 里赋值的字段一致(原 obj.value → 绑那个字段;Emp 原 obj.id → 绑 item.id);
      • 删掉原 @change 处理器;若原 change 除设字段外还做联动,把那部分挪到 el-select 的 native @change
      • 保留其它未点名的 ElementSelect 组件(Team/ProductionLine/Machine/Company 等)不动;
      • 若原组件用 :is-init-data="false" + 手动 getData(params):把 params 传给上面的 API 调用(在 created 或弹框打开方法里);若是动态联动(选 A 后调 this.$refs.x.getData(p) 重新加载 B 的选项):写一个 reloadX(params) 方法用相同 API 重新赋值本地数组,并在原触发点调用,替代 this.$refs.x.getData(...)
      • reset 方法里删掉 this.$refs.xxx.value='' 这类行(v-model 绑定字段会在重置对象里被清空)。
  • 收尾:替换完后务必再 grep 一次 PascalCase(OrganizationSelect|EmpSelect|ProductSupplierSelect),清掉残留的死 import / components 注册(不影响运行但要干净)。

P13. vue-router@4 同名 name 静默覆盖

  • 问题:路由配置里若两条路由 name 相同,后者静默覆盖前者,菜单点击对应项无反应(仿佛失效)。path 不动,只改 name
  • 修复:给同名路由加模块前缀(如 gjg1-xxx / gjg2-xxx),保证全局唯一。
  • 关联坑:路由 meta.roles 引用的角色必须在后端真实存在,否则整棵子树被静默过滤 → 404("幽灵角色"问题)。

P14. vue-clipboard3 需手动重注册 v-clipboard 指令

  • 问题:迁移后 v-clipboard 指令静默失效(复制没反应)。
  • 修复:在入口手动 app.directive('clipboard', Clipboard) 重新注册(Clipboard 来自 vue-clipboard3 的导出),否则指令不生效。

P15. require() 在 Vite / ESM 下未定义(访问即崩)★★★

  • 问题:Element Plus 项目里仍有几处用 require('element-ui/package.json')require('script-loader!jsonlint')require('minio') 等 CommonJS 写法,Vite 是 ESM,运行到该行直接 ReferenceError: require is not defined,对应页面打开即崩。
  • 排查grep -rn "require(" src/(排除 node_modules@/、相对路径的合法动态 import)。
  • 修复
    • 读取 JSON 版本:import pkg from 'element-ui/package.json' 然后 pkg.version(或 import { version } from 'element-ui' 若导出支持);
    • jsonlintnpm i jsonlint 后用 import jsonlint from 'jsonlint'
    • minio:改用 import * as Minio from 'minio'(注意 minio 是浏览器端 SDK,需确认打包兼容,必要时走后端代理)。
  • 仅这几页会崩,修完即可,不碰其它页面。
  • ⚠️ 关键辨析:require 只在文件被 import/执行时才崩。如果含 require() 的文件全项目没有任何地方 import 它(死代码),那行 require 永远不执行,当前不会崩——此时直接删除文件比改 require 更干净(先用 grep -rn "from '@/xxx'" src/ 确认 0 引用再删)。反之,若文件被业务页面引用(如 JsonEditor 被表单页用 component: () => import(...) 懒加载),则访问即崩,必须修。
  • 已验证的"真雷"判定法:先 grep 出所有 require(,再对每个命中文件 grep 其导入点——有活的 import = 真雷;无 import = 死代码可删。

P16. 组件注册 / 插件注册

  • 问题:Vue 2 的 Vue.use(XXX)new Vue() 写法在 Vue 3 不再适用。
  • 修复:入口改为 createApp(App).use(ElementPlus).use(Router).mount('#app');全局组件用 app.component('Xxx', Xxx) 注册。

P17. 函数式组件(functional)改写

  • 问题:Vue 2 functional: true 组件在 Vue 3 需重写为普通组件或用 <script setup> + 函数式渲染。
  • 修复:移除 functional: true,用 <script setup>render 函数(接收 props/ctx 而非 h, context)。

P18. echarts 放在 el-popover / el-tooltip 内:图表被裁剪 + 初始化时序 ★★★

  • 问题:页面"鼠标悬停弹出框显示图表"时,图表部分内容被裁剪、不在弹框内,或图表宽高为 0。两重根因:
    1. 弹框宽度不够<el-popover> 不设 width 时按内容自适应,但图表 div 是固定宽(如 width:600px),弹框比图表窄 → 图表溢出被裁。
    2. 初始化时机早:在 mounted 或数据返回时立即 echarts.init(dom),但 popover 此时 display:none/尚未布局,dom 尺寸为 0,init 拿到 0×0,图表画不出来或错位。
  • 排查
    grep -rn "echarts.init\|\$echarts.init" src/          # 找所有图表初始化点
    grep -rn "el-popover" src/ | grep -i "chart\|echarts" # 弹框内嵌图表
    
  • 修复(按下面 4 步):
    1. <el-popover> 设显式 width(≥ 图表宽度,如 width="640"),避免裁剪;
    2. 监听 popover 的 @show 事件,在显示后再初始化图表(不要在 mounted 直接 init);
    3. 每次显示前先 chart.dispose() 旧实例(或在 data 里缓存实例、显示时判断已存在则 dispose),避免实例叠加导致内存泄漏与渲染错乱;
    4. init 必须包在 this.$nextTick(() => { ... }) 里,确保 popover DOM 已展开、尺寸已就绪,最后调 chart.resize()
    // 推荐写法骨架
    chartShow(item) {
      this.$nextTick(() => {
        const el = this.$refs['chart' + item.type][0]
        if (!el) return
        const old = this['chart' + item.type]        // 缓存的旧实例
        if (old) old.dispose()
        const chart = this.$echarts.init(el)
        chart.setOption(/* ... */)
        chart.resize()
        this['chart' + item.type] = chart
      })
    }
    
    • 注意 v-forref 是数组,取 [0];循环内变量名不要和外层参数 item 冲突(改名 it)。
    • 若图表在 el-dialog 里而非 popover,同样要在 @openednextTick 再 init(dialog 也是先隐藏后显示)。

P19. 冗余/过时迁移文档与配置(项目整洁度,非运行时坑)

  • 问题:迁移过程中容易留下重复或过时的文档与配置:
    • 根目录散落的 MIGRATION.mddocuments/ 下的同名/同主题文档内容重叠,且有的把"未修复"写成"已修复"(误导后人);
    • 前端子目录自建的 .gitignore 与父目录 .gitignore 规则 100% 重复,造成维护双份;
    • 文档里声称某 require 已修,但代码里那行 require 还在(如 JsonEditor)。
  • 修复约定(跨项目通用):
    • 项目说明文档统一放一个目录(如 documents/),别在根目录散放;
    • 子目录不重复建 .gitignore,统一用父目录那份(gitignore 的无前导斜杠规则本就匹配任意层级);
    • 合并文档时先用 grep 核实代码真实状态,文档陈述必须以代码为准(尤其"已修复"类断言)。

P20. echarts 实例严禁放入 data()(Vue3 响应式劫持,致命)★★★

  • 问题:Vue 3 中 data() 返回的对象会被 reactive() 包成 Proxy。若把 echarts.init(dom) 的实例存进 data(),整个 ECharts 实例被深度响应式劫持,其内部 model/状态读写全部经 Proxy 转发;交互(点击图例 / 悬浮 tooltip)时 ECharts 解析 model.type 得到 undefined,抛 Cannot read properties of undefined (reading 'type')(echarts.js:1402)。与 Vue2/Vue3 无关,凡把图表实例塞进响应式状态都会触发
  • 排查
    grep -rn "data()" src/ | grep -i "chart\|echarts"   # 图表实例是否声明在 data()
    grep -rn "echarts.init\|\$echarts.init" src/         # 全部初始化点
    
  • 修复:图表实例保持在 Vue 响应式系统之外:
    // Options API:在 created() 里声明为普通实例属性(data() 之外的属性不会被 reactive 化)
    created() { this.xxxChart = null },
    methods: {
      initCharts() {
        const dom = this.$refs.xxxChartRef
        const chart = this.$echarts.init(dom)
        this.xxxChart = chart
      }
    }
    
    • 等价手段:shallowRef() / markRaw() 包裹实例亦可(const chart = shallowRef(null)markRaw(echarts.init(dom)))。
    • 数据同理:传给 setOption 的 series 数据若来自响应式对象(如 data.finished),先 .slice() 抽成普通数组(toPlainArray),避免响应式追踪/序列化干扰。
    • 迁移脚本坑(CRLF 项目尤其注意):把实例从 data() 搬到 created() 的脚本必须用逐行 split(/\r\n/) + 括号深度匹配定位,不要用"正则删 data 块"——正则会从 data 起匹配、不吃前导空格、破坏下一行缩进,且 data() 还有其它字段时只重建块内行会丢文件其余内容。正确做法:行级定位 data 起止 → 删字段行 → 空则整块删 → 按 mounted/methods 前导空格注入 created()

P21. el-dialog 与内部 el-table 不可同 ref

  • 问题:同一个 ref 名同时用在了 el-dialog 与它的子 el-table 上,Vue 的 ref 被后者覆盖,导致想拿 dialog 实例却拿到 table(或反之),getSelectionRows()、关闭等方法失效/报错。
  • 修复:dialog 与内部 table 用不同的 ref 名(如 ref="dialogRef"ref="tableRef")。

P22. Vue 3 已移除 this.$set → 直接赋值

  • 问题:Vue 2 的 this.$set(obj, key, val) 在 Vue 3 不存在,调用会抛 this.$set is not a function。Vue 3 响应式基于 Proxy,给响应式对象/数组的子属性直接赋值即可自动触发更新。
  • 修复
    // 错误
    this.$set(this.form, 'field', value)
    this.form.list[0].name = 'x'      // Vue2 需 $set,Vue3 直接写即可生效
    // 正确(Vue3)
    this.form.field = value
    this.form.list[0].name = 'x'
    
  • 排查:grep -rn "this.\$set\|\$set(" src/

P23. 自定义弹窗用 visible + update:visible 时,父组件须 v-model:visible(命名),不能裸 v-model

  • 背景:有些自定义弹窗组件没有modelValue,而是显式定义 visible prop + emit('update:visible', v)。这与 P9 的 modelValue 受控是两套约定
  • 问题:若子组件用 visible/update:visible,父组件却写v-model="x",裸 v-model 实际传的是 modelValue/update:modelValue,与子组件的 visible prop 对不上 → v-model 穿透到子组件内部的 el-dialog(因为 el-dialog 自己吃 modelValue),表现为弹框"空白 / 暂无数据"或根本不受控。
  • 修复:父组件用命名 v-model 匹配子组件的 prop 名:
    <!-- 子组件定义 props: { visible }, emits: ['update:visible'] -->
    <MyDialog v-model:visible="dialogVisible" />      <!-- 正确:对应 update:visible -->
    <!-- 错误:<MyDialog v-model="dialogVisible" />  会传 modelValue,不透不透 -->
    
    • 判断依据:看子组件 props 里是 modelValue 还是 visible——是 modelValue 用裸 v-model(P9),是 visiblev-model:visible(本模式)。
    • 关联原则(通用反模式):不要从父组件用 this.$refs.xxx.formData.xxx = ... 直改子组件内部响应式数据。子组件若 watchmodelValue/visible 在打开时 initFormData → resetForm,父级的直改会被清空;且弹窗内显示应读自己的 v-model。需要预填值就通过 prop/v-model 传,不要改内部状态。

4. 验证流程(迁移后必做)

项目约定:AI 不跑 vite build(除非用户贴出构建错误堆栈)。改动做完即止,由用户本地 npm run dev 验证。以下验证清单交给用户或自动化脚本。

4.1 控制台零报错

  • 用 Playwright / 浏览器打开关键页面,监听 consolepageerror
    • 重点确认没有 ReferenceError: require is not definedthis.$vnode is undefinedslot-scope 编译错误;
    • 确认没有因残留 this.$refs.xxxSelectRef.getData(...)(已删组件的 ref)导致的运行时报错。

4.2 弹框交互

  • 打开任意 el-dialog 弹框 → 点"取消"应关闭,点"提交/确认"执行完业务后也应关闭(验证 P9 的 v-model 受控是否回传父级)。

4.3 下拉框(自定义 select 包装组件替换后)

  • 宽度:原生 el-select 应显式 style="width:200px"(或父容器约束),不再塌陷;
  • 传参:选 Emp 后网络请求里 empId 应是真实的 item.id(不再是 undefined);
  • 联动:原 getData 联动(如选组织后重载团队/产线)仍正常工作。

4.4 表格 & 日期

  • 涉及多选的 el-tablegetSelectionRows() 返回正确选中行;
  • el-date-picker 选值后回显与向后端传参格式正确(大写 YYYY-MM-DD)。

4.5 验证清单模板(交付给用户)

每改完一批,输出:

文件绝对路径 | 原用法(组件名/ref/@change 处理器) | 新用法(原生 el-select 绑定字段 + 加载位置 + 是否保留 disabled/multiple/联动) | 备注

5. 常见"症状 → 原因 → 修复"速查表

| 症状 | 根因 | 对应模式 | |---|---|---| | 页面白屏 | 模板里写了 this. | P6 | | 打开页面直接崩 | require() 在 ESM 未定义 | P15 | | 弹框点取消/提交关不掉 | 子组件 visible 镜像与父级 v-model 不匹配 | P9 | | 下拉查不出数据 | Emp 取 obj.valueundefined,传参空 | P12 | | 下拉宽度塌陷 | 包装组件 width:100% 无显式宽 | P12 | | 日期选择乱码/无法选 | value-format 用了小写 y/d | P11 | | 菜单点击无反应 | 路由 name 重名被覆盖 | P13 | | 复制按钮没反应 | v-clipboard 指令未重注册 | P14 | | 表格拿不到选中行 | .selection 不可靠 | P10 | | 布局变松散 | size="mini" 失效 | P8 | | 编译报 Unexpected '/' | <style> 无 lang 用了 // | P7 | | 悬停弹框图表被裁剪/画不出 | el-popoverwidth + echarts 初始化早于布局 | P18 | | 图表交互崩 reading 'type' | echarts 实例存进 data() 被响应式劫持 | P20 | | 选不了行 / 关闭失效 | dialog 与内部 table 同 ref 被覆盖 | P21 | | this.$set is not a function | Vue3 无 $set,需直接赋值 | P22 | | 弹框"空白/暂无数据" | 子组件用 visible 但父级误用裸 v-model | P23 | | 文档说已修但实际未修 | 文档陈述未以代码 grep 为准 | P19 |


6. 通用注意事项(跨项目)

  • 不要改动项目里其它仍在用的 ElementSelect 包装组件(Team / ProductionLine / Machine / Company 等),只处理明确点名的三类。
  • 替换自定义 select 时,绑定字段必须与原 @change 赋值字段一致,否则表单提交会丢掉值。
  • 任何"先扫后改"——用第 2 节的 grep 建立清单,避免漏掉 PascalCase 死 import。
  • 迁移是分批进行的,每批改完抽页面本地验证,不要攒到最后一起验证。
  • 不要在本环境跑 vite build 做验证(除非用户提供构建错误);本地 npm run dev 才是验证入口。