一句话结论:
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
十、总结
- 三种状态不在同一层 :
disabled/readonly是控件能力,纯文本展示是渲染形态。 - readonly 不够:它不解决格式化、富内容、布局、校验、打印等问题。
- Element Plus 不内置是合理分工:基础组件库管控件,展示层由业务或上层封装补。
- 纯文本展示的价值在维护 :字段多了之后,一套模板比两套模板更不容易漂移;但字段少、布局完全不同的详情页,用
el-descriptions更合适。
如果决定做这层封装,接下来要处理的就是属性同步、校验和展示值推导三个问题。第 02、03、04 篇分别拆开讲。
npm: www.npmjs.com/package/ep-... 验证版本:Vue 3.5.x / Element Plus 2.14.x 完整实现参考:github.com/jktantan/ep...