通过
@chenglou/pretext库在前端精确计算文本宽度,实现了搜索表单输入框的动态宽度适配。告别min-width的粗暴限制,让 UI 更精致。
背景
做企业管理系统,搜索表单是最常见的组件。一个头疼的问题是:输入框宽度怎么定?
定死了,长文本挤成省略号;用 min-width,不同字段长度不同,结果参差不齐。
直到我发现了 pretext 这个库。
问题场景
看这个搜索表单:
<!-- SearchForm/index.vue -->
<template>
<div class="search-area">
<el-form :inline="true" :model="formData" class="search-form">
<el-form-item v-for="field in fields" :key="field.prop">
<el-input
v-if="field.type === 'input'"
v-model="formData[field.prop]"
:placeholder="field.placeholder || `请输入${field.label}`"
:style="getFieldStyle(field)"
/>
<!-- 日期范围选择 -->
<el-date-picker
v-else-if="field.type === 'dateRange'"
v-model="formData[field.prop]"
type="datetimerange"
:style="getFieldStyle(field)"
/>
<!-- 下拉选择 -->
<el-select
v-else-if="field.type === 'select'"
v-model="formData[field.prop]"
:placeholder="field.placeholder || `请选择${field.label}`"
:style="getFieldStyle(field)"
/>
</el-form-item>
</el-form>
</div>
</template>
字段配置是动态的:
const fields = [
{ prop: 'name', label: '姓名', type: 'input' },
{ prop: 'idCard', label: '身份证号', type: 'input' },
{ prop: 'createTime', label: '创建时间', type: 'dateRange' },
{ prop: 'status', label: '状态', type: 'select', options: [...] }
]
字段有长有短:
- "请输入姓名" → 短
- "请输入身份证号码" → 长
- "请选择开始时间 至 请选择结束时间" → 更长
核心问题:每个字段的 placeholder 长度不同,如何让输入框宽度刚刚好?
常见的"摆烂"方案
方案 1:固定宽度
.el-input {
width: 200px; /* 要不挤死,要不太空 */
}
方案 2:min-width
.el-input {
min-width: 180px;
width: auto;
}
结果就是参差不齐——"姓名"和"身份证号"都是 200px,但明显应该不同宽度。
方案 3:后端返回宽度配置
每个字段配一个宽度值,后端告诉我该多宽。
工作量大,而且字段改了要同步改配置。
解决方案:pretext 文本测量
@chenglou/pretext 是一个纯 JS 的文本渲染库,能精确计算给定字体样式下文本的像素宽度。
核心原理:
- 传入文本 + 字体样式
- 返回每个字符的位置信息
- 由此计算出文本总宽度
安装
yarn add @chenglou/pretext
核心代码
// fieldStyle.js
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext';
// 字体样式,与 Element Plus el-input 保持一致
const FONT_STYLE = '14px "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif';
/**
* 根据 placeholder 文本计算宽度
* @param {string} placeholderText - 占位符文本
* @param {number} extraPadding - 额外的内边距(默认 50px)
* @returns {number} 计算后的宽度
*/
export function getPlaceholderWidth(placeholderText, extraPadding = 50) {
if (!placeholderText) return 200;
const prepared = prepareWithSegments(placeholderText, FONT_STYLE);
const result = layoutWithLines(prepared, 1000, 14);
return Math.ceil(result.lines[0].width) + extraPadding;
}
/**
* 获取字段宽度样式
* @param {Object} field - 字段配置
* @returns {Object} 宽度样式对象 { width: string }
*/
export function getFieldStyle(field) {
let placeholderText = field.placeholder || `请输入${field.label}`;
let extraPadding = 50;
// dateRange 类型需要更宽(显示两个日期 + 分隔符)
if (field.type === 'dateRange') {
const startPlaceholder = field.startPlaceholder || `${field.label}开始时间`;
const endPlaceholder = field.endPlaceholder || `${field.label}结束时间`;
placeholderText = startPlaceholder + endPlaceholder;
extraPadding = 80; // dateRange 控件本身更宽
}
const width = getPlaceholderWidth(placeholderText, extraPadding);
return { width: `${width}px` };
}
使用效果
| 字段 | placeholder | 计算宽度 |
|---|---|---|
| 姓名 | 请输入姓名 | 132px |
| 身份证号 | 请输入身份证号 | 172px |
| 创建时间 | 请选择开始时间至请选择结束时间 | 340px |
输入框宽度自适应文本长度,视觉上整齐划一。
这样看是不是舒服多了!!!
踩坑记录
坑 1:字体必须完全一致
Element Plus 的 input 使用系统字体,如果 pretext 的字体定义和它不一致,计算出来的宽度会有偏差。
解决:直接从浏览器 DevTools 抄 Element Plus 的实际字体样式:
// Chrome DevTools Elements 面板
// 检查 .el-input__inner 的 computed styles
const FONT_STYLE = '14px "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif';
坑 2:dateRange 需要特殊处理
日期范围选择器显示的是两个日期 + "至"分隔符,宽度明显比普通输入框大。
解决:针对 dateRange 类型,把开始和结束的 placeholder 拼接起来计算,并且增大 extraPadding。
坑 3:计算结果偏小
单独计算每个字段后,实际渲染还是有点挤。
原因:输入框还有 padding、border 等自身宽度。
解决:加了 extraPadding = 50px 的缓冲,不同类型调整这个值。
完整组件代码
utils/fieldStyle.js
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext';
const FONT_STYLE = '14px "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif';
export function getPlaceholderWidth(placeholderText, extraPadding = 50) {
if (!placeholderText) return 200;
const prepared = prepareWithSegments(placeholderText, FONT_STYLE);
const result = layoutWithLines(prepared, 1000, 14);
return Math.ceil(result.lines[0].width) + extraPadding;
}
export function getFieldStyle(field) {
let placeholderText = field.placeholder || `请输入${field.label}`;
let extraPadding = 50;
if (field.type === 'dateRange') {
const startPlaceholder = field.startPlaceholder || `${field.label}开始时间`;
const endPlaceholder = field.endPlaceholder || `${field.label}结束时间`;
placeholderText = startPlaceholder + endPlaceholder;
extraPadding = 80;
}
const width = getPlaceholderWidth(placeholderText, extraPadding);
return { width: `${width}px` };
}
components/SearchForm/index.vue
<template>
<div class="search-area">
<el-form :inline="true" :model="formData" class="search-form">
<el-form-item v-for="field in fields" :key="field.prop">
<!-- 输入框 -->
<el-input
v-if="field.type === 'input'"
v-model="formData[field.prop]"
:placeholder="field.placeholder || `请输入${field.label}`"
clearable
:style="getFieldStyle(field)"
/>
<!-- 日期范围选择 -->
<el-date-picker
v-else-if="field.type === 'dateRange'"
v-model="formData[field.prop]"
type="datetimerange"
range-separator="至"
:start-placeholder="field.startPlaceholder || `${field.label}开始时间`"
:end-placeholder="field.endPlaceholder || `${field.label}结束时间`"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
:style="getFieldStyle(field)"
/>
<!-- 单个日期选择 -->
<el-date-picker
v-else-if="field.type === 'date'"
v-model="formData[field.prop]"
type="datetime"
:placeholder="field.placeholder || `请选择${field.label}`"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
:style="getFieldStyle(field)"
/>
<!-- 下拉选择 -->
<el-select
v-else-if="field.type === 'select'"
v-model="formData[field.prop]"
:placeholder="field.placeholder || `请选择${field.label}`"
clearable
:style="getFieldStyle(field)"
>
<el-option
v-for="option in field.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item class="search-btn-group">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, watch, reactive } from 'vue';
import { getFieldStyle } from '@/utils/fieldStyle';
const props = defineProps({
modelValue: { type: Object, default: () => ({}) },
fields: { type: Array, default: () => [] }
});
const emit = defineEmits(['update:modelValue', 'search', 'reset']);
const formData = reactive({ ...props.modelValue });
const initFormData = () => {
props.fields.forEach(field => {
if (!(field.prop in formData)) {
formData[field.prop] = field.type === 'dateRange' ? [] : '';
}
});
};
watch(() => props.fields, () => { initFormData(); }, { immediate: true, deep: true });
watch(() => props.modelValue, (val) => { Object.assign(formData, val); }, { deep: true });
const handleSearch = () => {
emit('search', { ...formData });
};
const handleReset = () => {
props.fields.forEach(field => {
formData[field.prop] = field.type === 'dateRange' ? [] : '';
});
emit('reset');
};
</script>
<style lang="scss" scoped>
.search-area {
background: #fff;
padding: 10px;
margin-bottom: 0;
border-radius: 4px;
.search-form {
.el-form-item {
margin-right: 10px;
margin-bottom: 12px;
}
}
}
</style>
对比效果
| 方案 | 姓名输入框 | 身份证号输入框 | 日期范围选择器 |
|---|---|---|---|
| 固定 200px | 200px (空旷) | 200px (刚好) | 200px (挤) |
| min-width: 180px | 180px (挤) | 200px+ (不统一) | 200px+ (不统一) |
| pretext 动态宽度 | 132px | 172px | 340px |
适用场景
适合用 pretext 的场景:
- 动态表单,字段配置来自后端
- 多语言系统,不同语言文本长度差异大
- 需要精细控制 UI 尺寸的企业级应用
不适合用的场景:
- 固定的几 个字段,直接配固定宽度更简单
- 性能敏感的热路径,pretext 计算有开销
- 响应式布局,容器宽度本身就在变
总结
pretext 解决了文本宽度计算的难题,让输入框宽度能"自适应"文本长度。核心就两个 API:
const prepared = prepareWithSegments(text, fontStyle);
const result = layoutWithLines(prepared, maxWidth, fontSize);
result.lines[0].width; // 文本宽度
配上调优的 extraPadding,基本能覆盖大部分场景。
有问题欢迎留言交流。