返回博客

Element UI el-select 显示原始值而非 label:v-model 类型严格匹配的隐形坑

某天接到一个诡异反馈:后台管理系统的筛选下拉框,第一次进页面显示正常,但把当前 URL 发给同事、或者 F5 刷新页面之后,下拉框里赫然显示的是 "1""2" 这样的原始值,而不是对应的文字 label。打开 Vue devtools,数据明明都在,没有任何 console 报错。

排查半小时发现:这不是 bug,是 Element UI 的设计——而我们踩进了它最容易忽视的类型陷阱。

现象

  • 页面初次加载(使用代码中的默认值):下拉框显示正确的 label,如 "实体门店"
  • 粘贴带 query string 的 URL 打开 / 刷新页面(filter 从 URL 恢复):下拉框变成 "1""2"
  • 没有任何报错,数据请求也正常发出——但后台收到的确实是字符串 "1" 而非数字 1

根因

Element UI(以及同源的 Element Plus)在匹配选中项时使用严格相等

// packages/select/src/option.vue(简化)
itemSelected() {
  return this.select.value === this.value
}

JavaScript 中 "1" !== 1,所以当 v-model 是字符串、但 :value="1" 是数字时,框架找不到匹配项,就把 v-model 的原始值直接渲染出来。

坑:vue-router $route.query 永远是字符串

vue-router 3 / 4 对 $route.query 的类型声明是 string | (string | null)[]从不做数字转换decodeURIComponent 同理,返回值恒为 string。

如果你的 selectlist 定义是:

selectlist: [
  { name: '实体门店', value: 1 },   // 数字字面量
  { name: '虚拟门店', value: 2 },
]

而 v-model 的来源是 $route.query.type(字符串 "1"),两者永远 === 不上。

这也是为什么第一次访问正常——初始响应式数据是 type: 1(数字),从未经历 URL 反序列化;而刷新/分享链接时就出问题——因为要从 query string 恢复。

常见的类型漂移边界不止 URL 一个:

| 漂移来源 | 机制 | |---|---| | $route.query | URL 永远是文本 | | localStorage.getItem | 永远返回 string | | JSON BIGINT 安全补丁 | 后端把大整数序列化为字符串 | | <input v-model> | DOM value 是 string | | 父组件 prop 透传 | 继承上游任一漂移来源 |

正解

在漂移边界处一次性转型,而不是在每个下拉组件里打补丁。

正解:在 URL/Storage 恢复层,对照 selectlist 自动转型

如果页面使用通用的表格搜索组件,在其 created 钩子里读取列配置里的 selectlist,自动推断期望类型并转换:

created() {
  // 扫描所有带 selectlist 的列,建立「字段 → 期望类型」映射
  const selectKeyTypes = {}
  this.columns.forEach((col) => {
    if (col.selectlist && col.selectlist.length > 0) {
      const k = col.searchKey || col.key
      selectKeyTypes[k] = typeof col.selectlist[0].value
    }
  })
 
  // 从 URL query 恢复时按类型转换
  for (const s in this.$route.query) {
    const raw = decodeURIComponent(this.$route.query[s])
    if (selectKeyTypes[s] === 'number' && raw !== '' && !isNaN(Number(raw))) {
      this.vars[s] = Number(raw)
    } else {
      this.vars[s] = raw
    }
  }
}

这样只要 selectlist 是字段值域的 SSoT,类型自动对齐,新增选项也不需要额外处理。

如果场景更简单(该下拉框只在当前页用),也可以直接把 option 值改成字符串,统一在请求层做 Number() 转换:

selectlist: [
  { name: '实体门店', value: '1' },  // 改为字符串
  { name: '虚拟门店', value: '2' },
]
// 发请求时:params.type = Number(this.vars.type)
修完别忘扫同域

同样的 selectlist + 数字字面量模式大概率在同一项目里还有多处。修完当前页,立即 grep 一遍所有 selectlist / options 定义,把所有 value: <数字> 的都检查一遍——它们是等待刷新/分享才暴露的定时炸弹。

验证

  1. 设置筛选条件 → 复制 URL → 新标签页粘贴打开 → 所有下拉框应显示 label 而非原始值
  2. F5 刷新 → label 依然正确
  3. Vue devtools 确认:typeof vars[key] === typeof selectlist[0].value
  4. Network 面板确认:请求参数类型符合后端期望(别把字符串 "1" 发给期望整数的后端)

一句话外卖

el-select 用 === 匹配,URL / Storage 边界永远产出字符串——在恢复层按 selectlist 类型转换一次,胜过在每个下拉框旁边打无数个补丁。