(一)为什么表单需要第三种状态:disabled、readonly 与纯文本展示

一句话结论:disabled 和 readonly 解决的是「能不能编辑」,纯文本展示解决的是「要不要渲染控件」;两者不在同一层。

一、一个详情页需求,为什么会变成两套模板

先看一个很普通的后台需求:订单详情页。

编辑页通常是这样:

html 复制代码
<template>
  <el-form :model="order" label-width="100px">
    <el-form-item label="客户姓名">
      <el-input v-model="order.customerName" />
    </el-form-item>

    <el-form-item label="订单状态">
      <el-select v-model="order.status">
        <el-option label="待付款" value="pending" />
        <el-option label="已付款" value="paid" />
        <el-option label="已发货" value="shipped" />
      </el-select>
    </el-form-item>

    <el-form-item label="下单时间">
      <el-date-picker v-model="order.createdAt" type="datetime" />
    </el-form-item>

    <el-form-item label="订单金额">
      <el-input-number v-model="order.amount" :precision="2" />
    </el-form-item>

    <el-form-item label="是否开票">
      <el-switch v-model="order.invoiced" />
    </el-form-item>

    <el-form-item label="备注">
      <el-input v-model="order.remark" type="textarea" />
    </el-form-item>
  </el-form>
</template>

详情页往往另写一套:

html 复制代码
<template>
  <el-descriptions :column="1" border>
    <el-descriptions-item label="客户姓名">
      {{ order.customerName }}
    </el-descriptions-item>

    <el-descriptions-item label="订单状态">
      {{ statusLabel(order.status) }}
    </el-descriptions-item>

    <el-descriptions-item label="下单时间">
      {{ formatDate(order.createdAt) }}
    </el-descriptions-item>

    <el-descriptions-item label="订单金额">
      {{ formatMoney(order.amount) }}
    </el-descriptions-item>

    <el-descriptions-item label="是否开票">
      {{ order.invoiced ? '是' : '否' }}
    </el-descriptions-item>

    <el-descriptions-item label="备注">
      {{ order.remark || '-' }}
    </el-descriptions-item>
  </el-descriptions>
</template>

一开始这没什么问题。真正的问题出现在需求变化之后:

  • 新增一个字段,要同时改编辑页和详情页;
  • 修改 label 文案,两处可能改得不一致;
  • 新增一个状态枚举,详情页的 statusLabel 忘了补;
  • 日期格式改了,编辑页和详情页显示不一样;
  • 权限从「可编辑」变成「只读」,详情页还要单独做一套权限判断;
  • 审批流里同一页面要在不同阶段切换编辑/只读;
  • 打印、导出、审计快照又要第三套展示逻辑。

这些问题的本质不是"多写了几行代码",而是同一个字段的展示规则被复制到了多个地方。复制之后,任何一次修改都可能只改一半。

很多团队会尝试用 readonly 解决:

html 复制代码
<el-form :model="order" label-width="100px">
  <el-form-item label="客户姓名">
    <el-input v-model="order.customerName" readonly />
  </el-form-item>
</el-form>

控件确实不能编辑了,但页面看起来仍然像一个表单,而不是详情页。而且 admin 还是 admin,日期还是原始值,金额也没有千分位。

从「两套模板」到「一套模板 + 展示态」的演进路径,可以这样理解:

二、三种状态到底差在哪

Element Plus 表单字段常见的三种状态:

状态 控制方式 渲染结果 可编辑 可聚焦 值格式化 校验
disabled el-form / el-form-item / 控件的 disabled 控件,禁用态 否 否 否 可能跳过
readonly 控件原生 readonly(例如 el-input 的 readonly) 控件,正常外观 否 通常是 否 仍可能校验
纯文本 自定义展示(v-if / 插槽 / 展示组件) 文本 否 否 需要自己实现 可跳过

三者的差异不只是「能不能改」,更在于渲染什么:

html 复制代码
<!-- disabled:控件还在,只是禁用 -->
<el-form-item label="客户姓名">
  <el-input v-model="order.customerName" disabled />
</el-form-item>

<!-- readonly:控件还在,可以聚焦,但不能改 -->
<el-form-item label="客户姓名">
  <el-input v-model="order.customerName" readonly />
</el-form-item>

<!-- 纯文本:控件不渲染,直接显示值 -->
<el-form-item label="客户姓名">
  <span>{{ order.customerName }}</span>
</el-form-item>

对应的 DOM 也完全不同:

html 复制代码
<!-- disabled -->
<input class="el-input__inner" disabled />

<!-- readonly -->
<input class="el-input__inner" readonly />

<!-- 纯文本 -->
<span>张三</span>

disabled 和 readonly 是控件能力 层面的状态,纯文本展示是渲染形态层面的状态。把它们混为一谈,就会出现「控件已经灰了,但页面还是不像详情页」的问题。

三种状态的层级关系可以概括为:

三、为什么 readonly 不够

1. 视觉上仍然是输入框

readonly 不会去掉输入框的边框、高度、内边距、后缀图标、清除按钮。详情页看起来仍然像一张可填写的表单,容易让用户误以为可以编辑。

html 复制代码
<el-input v-model="order.remark" readonly />

用户看到的是一个「不能输入的输入框」,而不是一段「只读文本」。这两个视觉预期不一样。

2. 布局仍然按控件尺寸占位

输入框有固定高度、内边距和边框。详情页如果字段很多,页面会显得很空,行与行之间的间距也不像文本段落那样紧凑。

readonly 不会改变控件的盒模型,所以布局问题依然存在。

3. readonly 不负责格式化

这是最容易被忽视的一点。readonly 只阻止编辑,不改变值:

html 复制代码
<!-- 显示 admin,而不是「管理员」 -->
<el-input v-model="order.status" readonly />

<!-- 显示 2024-05-20T00:00:00.000Z,而不是 2024-05-20 -->
<el-input v-model="order.createdAt" readonly />

详情页需要的是:

text 复制代码
待付款
2024-05-20 10:30:00
¥1,234.50
是

这些格式化逻辑不会因为加了 readonly 就自动出现。

4. 富内容无法用 readonly 输入框展示

详情页经常需要展示:

  • 状态 Tag;
  • 用户头像 + 姓名;
  • 文件列表;
  • 图片缩略图;
  • 评分星星;
  • 可点击的链接。

el-input readonly 只能显示字符串。要把 Tag、图标、图片塞进输入框,只能采用 hack 方式,而且会破坏输入框的语义。

5. 可访问性语义不对

readonly 的 <input> 仍然是输入控件:

  • 仍然可能进入 Tab 顺序;
  • 读屏软件仍然会把它读成文本框;
  • 用户仍然可以聚焦、选中、复制。

纯文本详情页的语义是"静态文本",不是"只读输入框"。两者的可访问性预期不同。

6. readonly 字段仍可能参与校验

readonly 不会自动把字段从 el-form 的校验中移除。如果字段有必填规则,validate() 仍然可能报错。

纯文本展示态通常需要跳过校验,或者至少不显示必填星号。这需要额外的字段注册控制,而 readonly 本身并不提供这种控制。

7. 打印和导出体验差

打印详情页时,输入框的边框、背景色和焦点样式都不适合纸质输出。纯文本更适合打印、PDF 导出和截图归档。

8. 交互预期不一致

详情页通常不需要输入框的焦点、光标和文本选择行为。readonly 保留了这些交互,用户会下意识地去点一下,然后发现不能改。

小结

readonly 解决的是「控件可见,但不能编辑」。它没有解决以下问题:

  • 是否渲染控件;
  • 值如何格式化;
  • 富内容如何展示;
  • 校验和必填星号如何处理;
  • 打印和导出如何排版。

这些属于展示层的问题,超出了控件属性所能覆盖的范围。

readonly 覆盖不到的问题,可以归纳为下面几类:

四、为什么 Element Plus 不内置

这里容易写成「官方为什么不做」,但更准确的说法是:从基础组件库的设计边界看,这件事不该由它来做。

1. 组件库负责控件,不负责业务展示模式

Element Plus 的职责是提供稳定、可组合的表单控件:

  • el-input 负责输入;
  • el-select 负责选择;
  • el-form-item 负责 label、布局、校验挂载。

"纯文本展示"不是某个控件的属性,而是一种跨组件的展示模式。它需要知道:

  • 当前字段用的是什么控件;
  • 控件的选项从哪里来;
  • 日期用什么格式;
  • 空值显示什么;
  • 是否需要跳过校验。

这些信息一部分在控件里,一部分在业务里。基础组件库很难替业务做决定。

2. 原生 readonly 的语义已经存在

HTML 的 readonly 语义是"控件可见,但用户不能修改"。它从来不是"不渲染控件"。

如果 Element Plus 再引入一个 text 模式,会和 disabled、readonly 形成三个容易混淆的概念。对基础组件库来说,API 的清晰度比功能数量更重要。

3. 展示值依赖业务上下文

同一个字段,不同业务的展示规则可能完全不同:

字段 业务 A 业务 B
status 字典映射中文 显示原始状态码
amount 千分位 + 货币 纯数字
date YYYY-MM-DD YYYY年MM月DD日
owner 显示姓名 显示邮箱
空值 - 暂无 / 空字符串

这些规则不适合硬编码到基础组件库中。

4. 详情展示已有 el-descriptions

Element Plus 已经提供了 el-descriptions:

html 复制代码
<el-descriptions :column="1" border>
  <el-descriptions-item label="客户姓名">张三</el-descriptions-item>
  <el-descriptions-item label="订单状态">待付款</el-descriptions-item>
</el-descriptions>

从组件库的角度看,详情页使用 descriptions 是合理的做法。它无需再为「表单字段的纯文本展示」维护一套平行 API。

5. 维护成本高

如果要内置,需要覆盖以下方面:

  • 几十个表单控件;
  • 单选、多选、范围、日期、上传、图片、评分等不同值类型;
  • 插槽和异步选项;
  • 校验注册与跳过;
  • SSR 首屏;
  • 主题、尺寸、暗色模式;
  • Element Plus 自身的版本兼容。

这是一整层展示逻辑,不是一个 prop 能解决的。

结论:不是做不到,是分层问题

基础组件库保证"控件行为一致",展示层由业务或上层组件封装。这个分工是合理的:

text 复制代码
Element Plus
  └── 控件、表单容器、校验、布局

业务层 / 展示层封装
  └── 取值、格式化、选项 label、空值、权限、展示形态

上层封装不是重复造轮子,而是补上基础组件库刻意不覆盖的那一层。

分层职责可以这样划分:

五、纯文本展示适合哪些业务场景

不是所有详情页都需要它。下面这些场景比较典型。

1. 详情页 / 查看页

最常见的场景。字段多、只读、需要统一的 label 和布局。

用 readonly 会保留输入框外观;用 el-descriptions 会换一套布局。如果希望详情页和编辑页保持同样的 label 宽度、间距、尺寸,纯文本展示态更合适。

2. 审批流 / 工单

同一个页面,不同阶段状态不同:

text 复制代码
草稿:可编辑
审批中:只读
已通过:只读
已驳回:可编辑

如果每个阶段都写一套模板,维护成本很高。通过一个 isText 状态切换展示形态,逻辑更集中。

3. 权限只读

用户仅有查看权限、没有编辑权限时:

ts 复制代码
const canEdit = computed(() => permissions.includes('order:edit'))

canEdit 为 false 时,页面应该展示纯文本,而不是一堆灰掉的输入框。

4. 提交前预览 / 确认页

用户填写完表单后,在提交前进行预览:

text 复制代码
姓名:张三
角色:管理员
入职日期:2024-05-20

预览页需要的是最终展示值,不是输入控件。复用同一份表单结构和格式化逻辑,可以避免「预览和提交后的详情页不一致」。

5. 打印 / 导出 / 快照

打印、PDF 导出、审计快照通常需要纯文本排版。输入框的边框、背景、焦点样式会干扰输出。

6. 表格行展开详情

表格行展开后展示详细信息。空间有限,用输入框会显得拥挤;纯文本更紧凑。

7. 审计日志 / 历史版本对比

展示某个时间点的字段值,通常是只读快照。需要格式化、空值文案、选项 label,但不需要控件。

8. 低代码 / 配置平台的只读渲染

低代码平台通常用 schema 描述表单。只读模式下,同一份 schema 应该能渲染成文本,而不是把所有控件禁用。

这些场景可以按"是否保留表单结构"来归类:

六、它能省掉哪些重复工作

先说明:纯文本展示态不一定会让初始代码量变少。如果只是两三个字段,写两套模板可能更快。

它的价值在字段数量增加、需求变化频繁之后。

维护点对比

维护点 两套模板 一套模板 + 展示态
字段定义 编辑页、详情页各一份 一份
label 文案 两处 一处
label 宽度 两套 一套
值格式化 两套函数 一套
权限判断 两套 一处状态
校验规则 两套 一套
主题 / 尺寸 两套 一套
新增字段 改两处 改一处
改字段文案 改两处,容易漏 改一处
打印 / 导出 可能第三套 复用文本态

一个具体的维护场景

假设产品要求把"订单状态"的显示从英文改成中文。

两套模板:

ts 复制代码
// 详情页
function statusLabel(status: string) {
  return { pending: '待付款', paid: '已付款', shipped: '已发货' }[status] ?? status
}

编辑页的 el-option 也需要同步修改。如果详情页新增了 refunded 状态,而编辑页忘记添加,两个页面就会不一致。

一套模板 + 展示态:

html 复制代码
<el-form-item label="订单状态">
  <span v-if="isText">{{ statusLabel(order.status) }}</span>
  <el-select v-else v-model="order.status">
    <el-option label="待付款" value="pending" />
    <el-option label="已付款" value="paid" />
    <el-option label="已发货" value="shipped" />
  </el-select>
</el-form-item>

状态枚举、label 文案和展示逻辑集中在同一个字段中,只需修改一处即可。

但初始代码量可能增加

纯文本展示需要处理以下内容:

  • v-if / v-else;
  • 空值文案;
  • 日期、金额、枚举格式化;
  • 多选分隔符;
  • 富内容插槽。

如果每个字段都手写一遍,代码量未必比维护两套模板少。所以实际项目通常会再封装一层:

html 复制代码
<TextField label="订单状态" prop="status">
  <el-select v-model="order.status">...</el-select>
</TextField>

封装层负责取值、格式化、空值处理、校验和展示形态;业务侧只需编写控件。

这就是「纯文本展示态」从「一个想法」发展为「一个可复用方案」的原因。

七、代价与边界

纯文本展示并非没有代价,需要明确其成本。

1. 切换状态可能重建表单项

为了让展示态跳过校验、编辑态恢复校验,有些实现会在切换时重建 el-form-item。代价是:

  • 切换瞬间的错误提示会被清除;
  • 字段内的局部状态(焦点、滚动位置)会重置;
  • 表单数据本身不受影响。

如果业务要求「切换时保留错误提示」,就不能简单重建,需要换一种控制方式。

2. SSR 首屏只能读初始值

服务端没有 MutationObserver,也无法监听 DOM 属性变化。首屏只能根据初始状态渲染。

如果首屏的展示形态依赖 window、屏幕宽度或客户端权限,服务端和客户端可能不一致,触发 hydration mismatch。

3. 富内容需要额外插槽

纯文本展示默认只能输出字符串,需要 Tag、图标、链接、图片时,要么提供 text 插槽,要么渲染组件自身的选中值插槽。

4. 自定义包装组件识别不了

如果插槽里放的是自定义组件:

html 复制代码
<TextField label="状态" prop="status">
  <StatusSelect v-model="order.status" />
</TextField>

封装层只能识别到 StatusSelect,无法识别其内部的 el-select,因此不能自动读取选项。需要显式传 options,或者用 formatter / text 插槽兜底。

5. 异步选项首屏可能回退

接口尚未返回时,选项为空,展示值可能暂时显示为原始值。待客户端选项返回后才会刷新。

6. 不适合所有详情页

以下场景用 el-descriptions 或专用模板更合适:

  • 详情页需要多列、分组、卡片布局;
  • 字段展示顺序和表单完全不一致;
  • 需要复杂的打印排版;
  • 需要展示大量非表单内容(时间线、日志、图表)。

纯文本展示态适合「保留表单结构,只切换字段的展示形态」的场景,并不是所有详情页的通用替代方案。

八、上层封装会遇到哪三个技术问题

如果你决定做一层展示封装,不管叫什么名字,都会碰到三个问题。

1. 属性怎么同步

给 el-form 传一个扩展属性控制展示形态,子组件怎么感知变化?

html 复制代码
<el-form :model="order" :text="isText">

el-form 没有声明 text,它只是 fallthrough attribute。子组件读 attrs 不会响应式更新。这就是第 02 篇要讲的:

  • attrs 为什么不是响应式;
  • 自定义 prop / provide/inject / MutationObserver 三种方案;
  • 长表单怎么共享 observer。

2. 校验怎么跳过和恢复

展示态不应该参与校验,也不应该显示必填星号。但给字段传 rules: [] 不够,因为表单级规则仍然生效。

这就是第 03 篇要讲的:

  • ElFormItem 的规则合并机制;
  • 为什么空 rules 无效;
  • 移除 prop + 清空 required + key 重建;
  • 怎么用 fields.length 和 validate() 验证。

3. 展示值怎么推导

拿不到子组件的 v-model,怎么知道 admin 应该显示成「管理员」?

这就是第 04 篇要讲的:

  • 遍历默认插槽 VNode;
  • 识别组件类型;
  • 从 options / data / el-option 收集选项;
  • 按值匹配 label;
  • 富内容和异步更新怎么处理。

这三个问题不是某个方案独有的,而是「在表单上做展示层」必然要面对的。

九、选型建议

需求 推荐
纯详情展示,不需要表单布局 el-descriptions
保留表单布局,编辑 / 展示切换 纯文本展示态
控件可见、不可编辑 readonly
表单暂不可提交,但保留控件 disabled
打印 / PDF / 审计快照 纯文本或专用打印模板
字段少、页面简单 两套模板也可以接受
详情布局与表单完全不同的 专用详情页组件
需要 Tag、图标、链接等富内容 展示态 + text 插槽

一个简单的判断:

text 复制代码
只需要展示,不需要表单布局     → el-descriptions
需要表单布局,且要切换编辑/展示 → 纯文本展示态
只是暂时不能编辑               → disabled
控件要可见但不能改             → readonly

十、总结

  1. 三种状态不在同一层 :disabled / readonly 是控件能力,纯文本展示是渲染形态。
  2. readonly 不够:它不解决格式化、富内容、布局、校验、打印等问题。
  3. Element Plus 不内置是合理分工:基础组件库管控件,展示层由业务或上层封装补。
  4. 纯文本展示的价值在维护 :字段多了之后,一套模板比两套模板更不容易漂移;但字段少、布局完全不同的详情页,用 el-descriptions 更合适。

如果决定做这层封装,接下来要处理的就是属性同步、校验和展示值推导三个问题。第 02、03、04 篇分别拆开讲。

npm: www.npmjs.com/package/ep-... 验证版本:Vue 3.5.x / Element Plus 2.14.x 完整实现参考:github.com/jktantan/ep...

相关推荐
李剑一1 小时前
为什么几乎所有的知名开源项目都是外国人写的?
前端·ai编程·deepseek
计算机魔术师1 小时前
1.4万亿没人接,OpenAI自己砍了1500亿
前端
溪语流沙2 小时前
【每天一个CSS | Day41】开关三连:一套状态机,三种视觉翻译
前端·css·html5·每日一练·按钮
变与不变8062 小时前
css定位布局精讲
前端·css
FRing032 小时前
JavaScript入门到精通三十三(异常处理、call方法、apply方法、bind方法、防抖、节流)
开发语言·前端·javascript
计算机魔术师3 小时前
OpenAI 砍掉 100 亿融资目标,AI 资本市场的温度计变了
前端
jktantan3 小时前
(二)Vue 3 的 attrs 不是响应式:el-form 扩展属性同步的三种方案
前端·javascript·vue.js·element-plus
前端 贾公子4 小时前
第 5 章:Codex 从可运行程序到可交付工具
java·前端·数据库
用户360055579004 小时前
Kestrel_VLLM - 26 · 发射空档三步曲:事件计时 → nsys → 判别实验
前端