输入框Input
在一行内收集单行文本(姓名、手机号、金额、验证码等)
数据录入 规格 27 · 输入框 Input 6 端实现 触摸优先
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">
<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-input.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Input.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
默认高度 44px、字号 16px:iOS 聚焦时不会自动放大页面。
查看代码(演示页原文 · 12 行)
<section class="demo-block" data-demo="basic">
<p class="demo-label">基础用法(默认高度 44px,字号 16px:iOS 聚焦不会放大页面;点行内任意位置即聚焦)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-basic">
<div class="kole-m-input">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" placeholder="请输入客户名称" aria-label="客户名称">
</label>
</div>
</div>
</div>
</section>单位写在框内,避免占位符里重复出现(前缀对读屏隐藏,语义由 label 承担)。
查看代码(演示页原文 · 13 行)
<section class="demo-block" data-demo="prefix">
<p class="demo-label">带前缀(单位写在框内,避免占位符里重复出现)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-prefix">
<div class="kole-m-input">
<label class="kole-m-input__hit">
<span class="kole-m-input__prefix" aria-hidden="true">¥</span>
<input class="kole-m-input__control" type="text" inputmode="decimal" value="1280.00" aria-label="金额">
</label>
</div>
</div>
</div>
</section>有值时右侧出现清除按钮;点它清空并把焦点交还输入框。
查看代码(演示页原文 · 14 行)
<section class="demo-block" data-demo="clearable">
<p class="demo-label">可清除(有值时右侧出现清除按钮,点它清空并收回按钮;清除按钮在 label 外,不会连带聚焦)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-clearable">
<div class="kole-m-input is-filled" id="in-clearable">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" value="20260920-001" aria-label="订单编号" id="in-clearable-field">
</label>
<button class="kole-m-input__clear" type="button" aria-label="清除" id="in-clearable-btn"
data-behavior="click-toggles-class:#in-clearable|is-filled">✕</button>
</div>
</div>
</div>
</section>default 44px 用于表单内多字段;large 52px 用于一行一字段。
查看代码(演示页原文 · 21 行)
<section class="demo-block" data-demo="size">
<p class="demo-label">尺寸两档(size=default 44px / size=large 52px;后者适合一行一字段)</p>
<div class="demo-box">
<div class="demo-stack" data-assert="input-size">
<div class="kole-m-field">
<div class="kole-m-input">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" placeholder="default(44px)" aria-label="默认尺寸">
</label>
</div>
</div>
<div class="kole-m-field">
<div class="kole-m-input kole-m-input--large">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" placeholder="large(52px)" aria-label="大尺寸">
</label>
</div>
</div>
</div>
</div>
</section>02 组件状态
is-error 边框变错误色,aria-invalid 置位,错误说明在框外下方。
查看代码(演示页原文 · 14 行)
<section class="demo-block" data-demo="error">
<p class="demo-label">错误态(is-error + aria-invalid:错误说明在框外下方,不挤占输入区)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-error">
<div class="kole-m-input is-error">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" value="138" aria-label="手机号"
aria-invalid="true" aria-describedby="in-error-msg">
</label>
</div>
<span class="kole-m-input__error" id="in-error-msg" role="alert">请输入 11 位手机号</span>
</div>
</div>
</section>整框置灰、输入区不可聚焦,读屏会跳过。
查看代码(演示页原文 · 12 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(整框置灰,输入区不可聚焦:读屏会跳过)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-disabled">
<div class="kole-m-input is-disabled">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" value="杭州仓(只读)" aria-label="仓库" disabled>
</label>
</div>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
value | string | '' | 受控值;有值时右侧才出现清除按钮(规格 §27.5) | N |
size | 'default' | 'large' | 'default' | 变体 size:default 高 44px,large 高 52px(规格 §27.3) | N |
placeholder | string | '' | 占位文字;它不算标签,标签由 label 给出(规格 §27.6) | N |
prefix | string | '' | 可选前缀,放单位或符号(规格 §27.2 prefix) | N |
clearable | boolean | false | 变体 clearable:有值时显示清除按钮(规格 §27.3) | N |
disabled | boolean | false | 状态 disabled:置灰且不可聚焦(规格 §27.4) | N |
error | string | '' | 状态 error:错误文案,非空时写 aria-invalid 并在框外下方显示(规格 §27.6) | N |
label | string | '输入框' | 无障碍名称,落到 aria-label(规格 §27.6) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
input | (value) | 输入时触发,回传当前值(规格 §27.5) |
confirm | (value) | 键盘「完成」键触发(规格 §27.5) |
clear | — | 点击清除按钮触发,值由宿主置空(规格 §27.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 框内右侧自定义内容(后缀:单位、显示密码等,规格 §27.2 suffix) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-input-height | var(--kole-m-touch-target) | 默认高度(≥ 44px 触控最小边长) |
--kole-m-input-duration | 150ms | 边框与聚焦外发光的过渡时长 |
何时使用
- 在一行内收集单行文本(姓名、手机号、金额、验证码等)
- 移动端输入框整行占满、高度不小于 44px,字号不小于 16px,否则 iOS 聚焦时会自动放大页面
- 清除动作必须在框内完成,因为触屏没有悬停的鼠标可以移开
- 键盘「完成」键触发 confirm 事件,值随 input 事件实时回传
- 错误说明放在框外下方,不挤占输入区
交互与触控
- 输入框高度不小于 44px;整行可点(把 .kole-m-input 放在 label 里,点行即聚焦)
- 清除按钮视觉是 16px 图标,热区外扩到 44px 最小触控边长
- 聚焦反馈是边框色 + 2px 外发光,150ms 过渡;不改变布局(不撑开高度)
- 有值时清除按钮才出现;清除后焦点留在输入框
- 键盘「完成」键触发 confirm 事件,值随 input 事件实时回传
无障碍
- 输入框用原生 input,名称由 aria-label 给出(占位文字不算标签)
- 错误态用 aria-invalid="true",错误文案用 aria-describedby 关联
- 清除按钮是原生 button 且带 aria-label="清除"
- 禁用态用原生 disabled,读屏会跳过
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 搜索框Search | 输入的目的是搜索时用搜索框(自带搜索图标与清除);普通文本收集用输入框 |
| 多行文本框Textarea | 内容可能超过一行时用多行文本框;单行文本用输入框 |
| 数字键盘NumberKeyboard | 金额/验证码等需要自定义键盘时,输入框配套数字键盘;键盘只回传按键,写入仍由宿主完成 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 输入内容的正则与业务校验(合法性判断在宿主) |
| 禁止发明 | inputmode 之外的自定义软键盘行为(键盘类型由宿主按场景指定) |
| 禁止发明 | 自动填充与验证码自动读取的策略 |
| 规格未定 | 密码是否需要内置「显示/隐藏」开关 |
| 规格未定 | 数字输入的千分位格式化时机 |
| 规格未定 | 前缀里是否允许放图片/图标 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
field | 字段容器,包裹输入框与下方错误提示(错误提示在框外,不挤占输入区) |
control | 原生 input,占满剩余宽度,字号 16px |
prefix | 可选前缀,放单位或符号(如「¥」) |
clear | 可选清除按钮,有值且 clearable=true 时出现在右侧 |
suffix | 可选后缀,由默认插槽给出的自定义内容(单位、显示密码等) |
errorText | 字段下方的错误提示文字 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
size | default / large | default (由数据驱动,无专属类) large .kole-m-input--large |
clearable | false / true | false (由数据驱动,无专属类) true .kole-m-input__clear |
代表变体
| 变体 | 标签 |
|---|---|
size=default · clearable=false | 默认输入框(44px,一行一字段) |
size=large · clearable=false | 大尺寸(52px,单字段成组) |
size=default · clearable=true | 可清除(有值时出现清除按钮) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Input.css · 纯样式(CSS) · 130 行
/* Kole UI Mobile · Input 样式 — 对齐移动端规格 §27
输入框:高度不小于 44px(--kole-m-touch-target),字号 16px(iOS 聚焦不自动放大);
有值且 clearable 时右侧出现清除按钮(视觉 16px、热区补到 44px);
错误态 is-error 与 aria-invalid 双写,错误文案在框外下方。 */
.kole-m-field {
box-sizing: border-box;
width: 100%;
}
.kole-m-input {
/* 组件级变量:业务侧可在容器上覆盖(如做紧凑表单) */
--kole-m-input-height: var(--kole-m-touch-target); /* 默认高度(≥ 44px 触控最小边长) */
--kole-m-input-duration: 150ms; /* 边框与聚焦外发光的过渡时长 */
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--kole-space-8);
width: 100%;
min-height: var(--kole-m-input-height);
padding: 0 var(--kole-space-12);
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-radius-base);
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: inherit;
font-size: var(--kole-m-font-size-body);
transition: border-color var(--kole-m-input-duration) var(--kole-ease-standard),
box-shadow var(--kole-m-input-duration) var(--kole-ease-standard);
}
/* 变体 size=large:一行只放一个字段时用(高度 52px) */
.kole-m-input--large { --kole-m-input-height: 52px; }
/* 状态 focus:聚焦反馈只改边框与外发光,不改高度(避免页面跳动) */
.kole-m-input:focus-within {
border-color: var(--kole-color-brand);
box-shadow: 0 0 0 2px var(--kole-color-focus-ring);
}
/* 状态 error:边框变错误色;聚焦时保留错误色,不再叠品牌色 */
.kole-m-input.is-error { border-color: var(--kole-color-error); }
.kole-m-input.is-error:focus-within { border-color: var(--kole-color-error); }
/* 状态 disabled:整框置灰,输入区不可聚焦 */
.kole-m-input.is-disabled {
background: var(--kole-color-disabled-bg);
color: var(--kole-color-text-disabled);
cursor: not-allowed;
}
/* 整行热区:把「前缀 + 输入区」包在 label 里,点左侧任意位置即聚焦;
label 里**不能**放 labelable 元素(button 就是),所以清除按钮必须在 label 外 ——
否则点击清除会同时激活 label,既触发两次语义又是不合法嵌套。 */
.kole-m-input__hit {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
gap: var(--kole-space-8);
cursor: text;
}
.kole-m-input__prefix,
.kole-m-input__suffix {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
.kole-m-input__control {
flex: 1 1 auto;
min-width: 0;
height: var(--kole-m-input-height);
padding: 0;
border: 0;
outline: none;
background: transparent;
color: inherit;
font-family: inherit;
font-size: var(--kole-m-font-size-body);
line-height: 1.4;
}
.kole-m-input__control::placeholder { color: var(--kole-color-text-placeholder); }
.kole-m-input__control:disabled { cursor: not-allowed; color: var(--kole-color-text-disabled); }
/* 禁用态:整行不再是文本光标 */
.kole-m-input.is-disabled .kole-m-input__hit { cursor: not-allowed; }
/* 状态 is-filled:有值时清除按钮才出现(无值时不占位) */
.kole-m-input__clear {
flex: 0 0 auto;
display: none;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--kole-m-touch-target);
height: var(--kole-m-touch-target);
margin-right: calc(var(--kole-space-12) * -1);
padding: 0;
border: 0;
background: none;
color: var(--kole-color-text-placeholder);
font-size: var(--kole-m-font-size-label);
line-height: 1;
cursor: pointer;
touch-action: manipulation;
}
.kole-m-input.is-filled .kole-m-input__clear { display: inline-flex; }
.kole-m-input__clear:active { color: var(--kole-color-text-secondary); }
.kole-m-input__clear:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: -2px;
}
.kole-m-input__error {
display: block;
margin-top: var(--kole-space-8);
font-size: var(--kole-m-font-size-label);
line-height: 1.4;
color: var(--kole-color-error);
}
frameworks-mobile/Input.html · H5 原生(无框架) · 165 行
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Kole UI Mobile · Input(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Input.css">
<style>
body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
.demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 0; }
.demo-label { margin: 0; padding: var(--kole-space-12) var(--kole-m-gutter) var(--kole-space-8);
font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); }
.demo-box { padding: var(--kole-m-gutter); background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-stack { display: flex; flex-direction: column; gap: var(--kole-space-16); }
.demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="basic">
<p class="demo-label">基础用法(默认高度 44px,字号 16px:iOS 聚焦不会放大页面;点行内任意位置即聚焦)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-basic">
<div class="kole-m-input">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" placeholder="请输入客户名称" aria-label="客户名称">
</label>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="prefix">
<p class="demo-label">带前缀(单位写在框内,避免占位符里重复出现)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-prefix">
<div class="kole-m-input">
<label class="kole-m-input__hit">
<span class="kole-m-input__prefix" aria-hidden="true">¥</span>
<input class="kole-m-input__control" type="text" inputmode="decimal" value="1280.00" aria-label="金额">
</label>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="clearable">
<p class="demo-label">可清除(有值时右侧出现清除按钮,点它清空并收回按钮;清除按钮在 label 外,不会连带聚焦)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-clearable">
<div class="kole-m-input is-filled" id="in-clearable">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" value="20260920-001" aria-label="订单编号" id="in-clearable-field">
</label>
<button class="kole-m-input__clear" type="button" aria-label="清除" id="in-clearable-btn"
data-behavior="click-toggles-class:#in-clearable|is-filled">✕</button>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="size">
<p class="demo-label">尺寸两档(size=default 44px / size=large 52px;后者适合一行一字段)</p>
<div class="demo-box">
<div class="demo-stack" data-assert="input-size">
<div class="kole-m-field">
<div class="kole-m-input">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" placeholder="default(44px)" aria-label="默认尺寸">
</label>
</div>
</div>
<div class="kole-m-field">
<div class="kole-m-input kole-m-input--large">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" placeholder="large(52px)" aria-label="大尺寸">
</label>
</div>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="error">
<p class="demo-label">错误态(is-error + aria-invalid:错误说明在框外下方,不挤占输入区)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-error">
<div class="kole-m-input is-error">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" value="138" aria-label="手机号"
aria-invalid="true" aria-describedby="in-error-msg">
</label>
</div>
<span class="kole-m-input__error" id="in-error-msg" role="alert">请输入 11 位手机号</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(整框置灰,输入区不可聚焦:读屏会跳过)</p>
<div class="demo-box">
<div class="kole-m-field" data-assert="input-disabled">
<div class="kole-m-input is-disabled">
<label class="kole-m-input__hit">
<input class="kole-m-input__control" type="text" value="杭州仓(只读)" aria-label="仓库" disabled>
</label>
</div>
</div>
</div>
</section>
</div>
<script>
/* 演示页脚本:真实的值 ↔ 状态联动。
- 输入时:有值 → 根加 is-filled(清除按钮出现);无值 → 去掉
- 点清除:清空输入框、去掉 is-filled,并把焦点交还输入框
真实业务里这份状态由宿主管理(受控 value + input 事件),此处是最小可运行实现。 */
(function () {
Array.prototype.forEach.call(document.querySelectorAll('.kole-m-input'), function (root) {
var control = root.querySelector('.kole-m-input__control');
if (!control) return;
function sync() {
root.classList.toggle('is-filled', String(control.value || '').length > 0);
}
control.addEventListener('input', sync);
sync();
});
var clearBtn = document.getElementById('in-clearable-btn');
if (clearBtn) {
clearBtn.addEventListener('click', function () {
var root = document.getElementById('in-clearable');
var control = document.getElementById('in-clearable-field');
if (control) {
control.value = '';
control.focus();
}
if (root) root.classList.remove('is-filled');
});
}
})();
</script>
<script>
/* ?demo=<id> → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
(function () {
var id = new URLSearchParams(location.search).get('demo');
if (!id) return;
var blocks = Array.prototype.slice.call(document.querySelectorAll('.demo-block'));
var hit = false;
blocks.forEach(function (b) {
var on = b.getAttribute('data-demo') === id;
if (on) hit = true;
b.hidden = !on;
});
if (!hit) { blocks.forEach(function (b) { b.hidden = false; }); return; }
document.body.classList.add('demo-single');
blocks.forEach(function (b) {
var label = b.querySelector('.demo-label');
if (label && !b.hidden) label.hidden = true;
});
})();
</script>
</body>
</html>
frameworks-mobile/Input.jsx · React · 86 行
import React from 'react';
import './Input.css';
/* 输入框(移动端)— 规格 §27
结构:field(容器)> input(框)> prefix / control / clear + error(框外提示)。
控件是原生 input:键盘、输入法、autofill 都用系统能力,组件不自造输入法。
有值且 clearable 时才出现清除按钮;错误态同时写 is-error 与 aria-invalid,
错误文案用 aria-describedby 关联(规格 §27.6)。 */
export default function Input({
value = '',
size = 'default',
placeholder = '',
prefix = '',
clearable = false,
disabled = false,
error = '',
label = '输入框',
onInput,
onConfirm,
onClear,
children = null,
}) {
const filled = String(value || '').length > 0;
const fieldCls =
'kole-m-input' +
(size === 'large' ? ' kole-m-input--large' : '') +
(filled ? ' is-filled' : '') +
(error ? ' is-error' : '') +
(disabled ? ' is-disabled' : '');
const errorId = 'kole-m-input-error';
return (
<div className="kole-m-field">
<div className={fieldCls}>
{/* 整行热区是 label;清除按钮是 labelable 元素(button),必须在 label 外 */}
<label className="kole-m-input__hit">
{prefix ? (
<span className="kole-m-input__prefix" aria-hidden="true">
{prefix}
</span>
) : null}
<input
className="kole-m-input__control"
type="text"
value={value}
placeholder={placeholder}
disabled={disabled}
aria-label={label}
aria-invalid={error ? 'true' : undefined}
aria-describedby={error ? errorId : undefined}
onChange={(e) => {
if (disabled) return;
if (onInput) onInput(e.target.value);
}}
onKeyDown={(e) => {
if (disabled) return;
if (e.key === 'Enter' && onConfirm) onConfirm(value);
}}
/>
</label>
{clearable && filled && !disabled ? (
<button
className="kole-m-input__clear"
type="button"
aria-label="清除"
onClick={() => {
if (onClear) onClear();
}}
>
✕
</button>
) : null}
{children ? <span className="kole-m-input__suffix">{children}</span> : null}
</div>
{error ? (
<span className="kole-m-input__error" id={errorId} role="alert">
{error}
</span>
) : null}
</div>
);
}
frameworks-mobile/Input.vue2.vue · Vue 2 · 84 行
<template>
<div class="kole-m-field">
<div class="kole-m-input" :class="fieldClass">
<!-- 整行热区是 label;清除按钮是 labelable 元素(button),必须在 label 外 -->
<label class="kole-m-input__hit">
<span v-if="prefix" class="kole-m-input__prefix" aria-hidden="true">{{ prefix }}</span>
<input
class="kole-m-input__control"
type="text"
:value="value"
:placeholder="placeholder"
:disabled="disabled"
:aria-label="label"
:aria-invalid="error ? 'true' : null"
:aria-describedby="error ? errorId : null"
@input="onInput"
@keydown.enter="$emit('confirm', value)"
/>
</label>
<button
v-if="clearable && filled && !disabled"
class="kole-m-input__clear"
type="button"
aria-label="清除"
@click="$emit('clear')"
>
✕
</button>
<span v-if="$slots.default" class="kole-m-input__suffix"><slot></slot></span>
</div>
<span v-if="error" class="kole-m-input__error" :id="errorId" role="alert">{{ error }}</span>
</div>
</template>
<script>
/* 输入框(移动端)— 规格 §27
结构:field(容器)> input(框)> prefix / control / clear + error(框外提示)。
控件是原生 input:键盘、输入法、autofill 都用系统能力,组件不自造输入法。
有值且 clearable 时才出现清除按钮;错误态同时写 is-error 与 aria-invalid,
错误文案用 aria-describedby 关联(规格 §27.6)。 */
/* 错误提示元素 id 的自增种子:同页多实例不撞号(不依赖内部实例字段) */
var errorSeed = 0;
export default {
name: 'KoleMInput',
props: {
value: { type: String, default: '' },
size: { type: String, default: 'default' },
placeholder: { type: String, default: '' },
prefix: { type: String, default: '' },
clearable: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
error: { type: String, default: '' },
label: { type: String, default: '输入框' }
},
data: function () {
errorSeed += 1;
return { errorId: 'kole-m-input-error-' + errorSeed };
},
computed: {
filled: function () {
return String(this.value || '').length > 0;
},
fieldClass: function () {
return [
this.size === 'large' ? 'kole-m-input--large' : '',
this.filled ? 'is-filled' : '',
this.error ? 'is-error' : '',
this.disabled ? 'is-disabled' : ''
].filter(Boolean);
}
},
methods: {
onInput: function (e) {
if (this.disabled) return;
this.$emit('input', e.target.value);
}
}
};
</script>
<style src="./Input.css"></style>
frameworks-mobile/Input.vue3.vue · Vue 3 · 74 行
<template>
<div class="kole-m-field">
<div class="kole-m-input" :class="fieldClass">
<!-- 整行热区是 label;清除按钮是 labelable 元素(button),必须在 label 外 -->
<label class="kole-m-input__hit">
<span v-if="prefix" class="kole-m-input__prefix" aria-hidden="true">{{ prefix }}</span>
<input
class="kole-m-input__control"
type="text"
:value="value"
:placeholder="placeholder"
:disabled="disabled"
:aria-label="label"
:aria-invalid="error ? 'true' : null"
:aria-describedby="error ? errorId : null"
@input="onInput"
@keydown.enter="emit('confirm', value)"
/>
</label>
<button
v-if="clearable && filled && !disabled"
class="kole-m-input__clear"
type="button"
aria-label="清除"
@click="emit('clear')"
>
✕
</button>
<span v-if="$slots.default" class="kole-m-input__suffix"><slot></slot></span>
</div>
<span v-if="error" class="kole-m-input__error" :id="errorId" role="alert">{{ error }}</span>
</div>
</template>
<script setup>
/* 输入框(移动端)— 规格 §27
结构:field(容器)> input(框)> prefix / control / clear + error(框外提示)。
控件是原生 input:键盘、输入法、autofill 都用系统能力,组件不自造输入法。
有值且 clearable 时才出现清除按钮;错误态同时写 is-error 与 aria-invalid,
错误文案用 aria-describedby 关联(规格 §27.6)。 */
import { computed, getCurrentInstance } from 'vue';
const props = defineProps({
value: { type: String, default: '' },
size: { type: String, default: 'default' },
placeholder: { type: String, default: '' },
prefix: { type: String, default: '' },
clearable: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
error: { type: String, default: '' },
label: { type: String, default: '输入框' }
});
const emit = defineEmits(['input', 'confirm', 'clear']);
/* 错误提示元素 id:取当前实例 uid,保证同页多实例不撞号 */
const errorId = 'kole-m-input-error-' + getCurrentInstance().uid;
const filled = computed(() => String(props.value || '').length > 0);
const fieldClass = computed(() => [
props.size === 'large' ? 'kole-m-input--large' : '',
filled.value ? 'is-filled' : '',
props.error ? 'is-error' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function onInput(e) {
if (props.disabled) return;
emit('input', e.target.value);
}
</script>
<style src="./Input.css"></style>
frameworks-mobile/Input.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 157 行
<template>
<view class="kole-m-field">
<view class="kole-m-input" :class="fieldClass">
<text v-if="prefix" class="kole-m-input__prefix">{{ prefix }}</text>
<input
class="kole-m-input__control"
type="text"
:value="value"
:placeholder="placeholder"
:disabled="disabled"
:aria-label="label"
:aria-invalid="error ? 'true' : 'false'"
placeholder-class="kole-m-input__placeholder"
@input="onInput"
@confirm="onConfirm"
/>
<view
v-if="clearable && filled && !disabled"
class="kole-m-input__clear"
role="button"
aria-label="清除"
@tap="clear"
>
<text>✕</text>
</view>
<slot></slot>
</view>
<text v-if="error" class="kole-m-input__error">{{ error }}</text>
</view>
</template>
<script setup>
/* uni-app 端 · 输入框(移动端)— 规格 §27
跨端差异:
① <input> 是 uni 基础组件,事件对象是 { detail: { value } },**没有 DOM 的 event.target**
(这是本批次最容易踩的坑,照 frameworks-uniapp-pc/Input.uniapp.vue 的形态写);
② 清除按钮用 view + role="button"(小程序没有可聚焦的原生 button 语义),点击用 @tap;
③ 键盘「完成」键是 @confirm,不是 keydown.enter。
尺寸用 rpx:88rpx = 375pt 下的 44px 触控最小边长,故默认高 88rpx、large 104rpx。 */
import { computed } from 'vue';
const props = defineProps({
value: { type: String, default: '' },
size: { type: String, default: 'default' },
placeholder: { type: String, default: '' },
prefix: { type: String, default: '' },
clearable: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
error: { type: String, default: '' },
label: { type: String, default: '输入框' }
});
const emit = defineEmits(['input', 'confirm', 'clear']);
const filled = computed(() => String(props.value || '').length > 0);
const fieldClass = computed(() => [
props.size === 'large' ? 'kole-m-input--large' : '',
filled.value ? 'is-filled' : '',
props.error ? 'is-error' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
/* uni 的 <input> 事件对象为 { detail: { value } },无 DOM event.target */
function onInput(e) {
if (props.disabled) return;
emit('input', e && e.detail ? e.detail.value : '');
}
function onConfirm(e) {
if (props.disabled) return;
emit('confirm', e && e.detail ? e.detail.value : props.value);
}
function clear() {
if (props.disabled) return;
emit('clear');
}
</script>
<style>
.kole-m-field {
box-sizing: border-box;
width: 100%;
}
.kole-m-input {
--kole-m-input-height: 88rpx; /* 默认高度(88rpx = 44px,触控最小边长) */
--kole-m-touch-target: 88rpx;
--kole-m-font-size-body: 32rpx;
--kole-m-font-size-label: 28rpx;
--kole-m-gutter: 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
min-height: var(--kole-m-input-height);
padding: 0 24rpx;
border: 1rpx solid var(--kole-color-border);
border-radius: 8rpx;
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-body);
}
/* 变体 size=large */
.kole-m-input--large { --kole-m-input-height: 104rpx; }
/* 状态 focus:聚焦反馈只改边框,不改高度 */
.kole-m-input:focus-within { border-color: var(--kole-color-brand); }
/* 状态 error */
.kole-m-input.is-error { border-color: var(--kole-color-error); }
/* 状态 disabled */
.kole-m-input.is-disabled {
background-color: var(--kole-color-disabled-bg);
color: var(--kole-color-text-disabled);
}
.kole-m-input__prefix {
flex-shrink: 0;
padding-right: 12rpx;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
.kole-m-input__control {
flex: 1;
min-width: 0;
height: var(--kole-m-input-height);
font-size: var(--kole-m-font-size-body);
color: inherit;
}
.kole-m-input__placeholder { color: var(--kole-color-text-placeholder); }
/* 清除按钮:视觉小、热区 88rpx */
.kole-m-input__clear {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
margin-right: -24rpx;
color: var(--kole-color-text-placeholder);
font-size: var(--kole-m-font-size-label);
}
.kole-m-input__error {
display: block;
margin-top: 12rpx;
font-size: var(--kole-m-font-size-label);
color: var(--kole-color-error);
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 17 条 · 全部通过 报告 2026-09-22 23:01:05
node site/dev-server.js &
REG_BASE=http://127.0.0.1:3311 node tools/run-mobile-regression.mjs # 全量 5 个组件
npm run verify:mobile-docs # 本页内容完整性 + API 与源码一致性
设计契约
components/mobile-input.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "27 · 输入框 Input",
"confidence": "high",
"slug": "mobile-input",
"name": "输入框 Input",
"semanticTypeCandidates": [
"input",
"text-field",
"form-control"
],
"variantDimensions": [
{
"name": "size",
"values": [
"default",
"large"
]
},
{
"name": "clearable",
"values": [
"false",
"true"
]
}
],
"representativeVariants": [
{
"size": "default",
"clearable": "false",
"label": "默认输入框(44px,一行一字段)"
},
{
"size": "large",
"clearable": "false",
"label": "大尺寸(52px,单字段成组)"
},
{
"size": "default",
"clearable": "true",
"label": "可清除(有值时出现清除按钮)"
}
],
"anatomy": {
"field": "字段容器,包裹输入框与下方错误提示(错误提示在框外,不挤占输入区)",
"control": "原生 input,占满剩余宽度,字号 16px",
"prefix": "可选前缀,放单位或符号(如「¥」)",
"clear": "可选清除按钮,有值且 clearable=true 时出现在右侧",
"suffix": "可选后缀,由默认插槽给出的自定义内容(单位、显示密码等)",
"errorText": "字段下方的错误提示文字"
},
"structurePatterns": {
"size": "default(高度 44px)/ large(高度 52px)",
"clearable": "false(无清除按钮)/ true(有值时右侧出现清除按钮)"
},
"usageHints": [
"在一行内收集单行文本(姓名、手机号、金额、验证码等)",
"移动端输入框整行占满、高度不小于 44px,字号不小于 16px,否则 iOS 聚焦时会自动放大页面",
"清除动作必须在框内完成,因为触屏没有悬停的鼠标可以移开",
"键盘「完成」键触发 confirm 事件,值随 input 事件实时回传",
"错误说明放在框外下方,不挤占输入区"
],
"doNotInvent": [
"输入内容的正则与业务校验(合法性判断在宿主)",
"inputmode 之外的自定义软键盘行为(键盘类型由宿主按场景指定)",
"自动填充与验证码自动读取的策略"
],
"unknowns": [
"密码是否需要内置「显示/隐藏」开关",
"数字输入的千分位格式化时机",
"前缀里是否允许放图片/图标"
],
"interaction": [
"输入框高度不小于 44px;整行可点(把 .kole-m-input 放在 label 里,点行即聚焦)",
"清除按钮视觉是 16px 图标,热区外扩到 44px 最小触控边长",
"聚焦反馈是边框色 + 2px 外发光,150ms 过渡;不改变布局(不撑开高度)",
"有值时清除按钮才出现;清除后焦点留在输入框",
"键盘「完成」键触发 confirm 事件,值随 input 事件实时回传"
],
"accessibility": [
"输入框用原生 input,名称由 aria-label 给出(占位文字不算标签)",
"错误态用 aria-invalid=\"true\",错误文案用 aria-describedby 关联",
"清除按钮是原生 button 且带 aria-label=\"清除\"",
"禁用态用原生 disabled,读屏会跳过"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "value",
"type": "string",
"default": "''",
"desc": "受控值;有值时右侧才出现清除按钮(规格 §27.5)",
"required": false
},
{
"name": "size",
"type": "'default' | 'large'",
"default": "'default'",
"desc": "变体 size:default 高 44px,large 高 52px(规格 §27.3)",
"required": false
},
{
"name": "placeholder",
"type": "string",
"default": "''",
"desc": "占位文字;它不算标签,标签由 label 给出(规格 §27.6)",
"required": false
},
{
"name": "prefix",
"type": "string",
"default": "''",
"desc": "可选前缀,放单位或符号(规格 §27.2 prefix)",
"required": false
},
{
"name": "clearable",
"type": "boolean",
"default": "false",
"desc": "变体 clearable:有值时显示清除按钮(规格 §27.3)",
"required": false
},
{
"name": "disabled",
"type": "boolean",
"default": "false",
"desc": "状态 disabled:置灰且不可聚焦(规格 §27.4)",
"required": false
},
{
"name": "error",
"type": "string",
"default": "''",
"desc": "状态 error:错误文案,非空时写 aria-invalid 并在框外下方显示(规格 §27.6)",
"required": false
},
{
"name": "label",
"type": "string",
"default": "'输入框'",
"desc": "无障碍名称,落到 aria-label(规格 §27.6)",
"required": false
}
],
"events": [
{
"name": "input",
"params": "(value)",
"desc": "输入时触发,回传当前值(规格 §27.5)"
},
{
"name": "confirm",
"params": "(value)",
"desc": "键盘「完成」键触发(规格 §27.5)"
},
{
"name": "clear",
"params": "—",
"desc": "点击清除按钮触发,值由宿主置空(规格 §27.5)"
}
],
"slots": [
{
"name": "default",
"desc": "框内右侧自定义内容(后缀:单位、显示密码等,规格 §27.2 suffix)"
}
]
},
"variantClasses": {
"size": {
"default": [],
"large": [
".kole-m-input--large"
]
},
"clearable": {
"false": [],
"true": [
".kole-m-input__clear"
]
}
},
"demos": [
{
"id": "basic",
"group": "01 组件类型",
"title": "基础用法",
"desc": "默认高度 44px、字号 16px:iOS 聚焦时不会自动放大页面。",
"variant": "size=default"
},
{
"id": "prefix",
"group": "01 组件类型",
"title": "带前缀",
"desc": "单位写在框内,避免占位符里重复出现(前缀对读屏隐藏,语义由 label 承担)。",
"variant": "prefix"
},
{
"id": "clearable",
"group": "01 组件类型",
"title": "可清除",
"desc": "有值时右侧出现清除按钮;点它清空并把焦点交还输入框。",
"variant": "clearable=true"
},
{
"id": "size",
"group": "01 组件类型",
"title": "尺寸两档",
"desc": "default 44px 用于表单内多字段;large 52px 用于一行一字段。",
"variant": "size=default|large"
},
{
"id": "error",
"group": "02 组件状态",
"title": "错误态",
"desc": "is-error 边框变错误色,aria-invalid 置位,错误说明在框外下方。",
"variant": "状态 error"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "禁用",
"desc": "整框置灰、输入区不可聚焦,读屏会跳过。",
"variant": "disabled=true"
}
],
"related": [
{
"slug": "mobile-search",
"why": "输入的目的是搜索时用搜索框(自带搜索图标与清除);普通文本收集用输入框"
},
{
"slug": "mobile-textarea",
"why": "内容可能超过一行时用多行文本框;单行文本用输入框"
},
{
"slug": "mobile-numberkeyboard",
"why": "金额/验证码等需要自定义键盘时,输入框配套数字键盘;键盘只回传按键,写入仍由宿主完成"
}
]
}