移动端导航输入框

输入框Input

在一行内收集单行文本(姓名、手机号、金额、验证码等)

数据录入 规格 27 · 输入框 Input 6 端实现 触摸优先

引入(H5 原生;其余 5 端见「快速开始」)
<!-- ① 令牌: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 组件类型

基础用法size=default

默认高度 44px、字号 16px:iOS 聚焦时不会自动放大页面。

查看代码(演示页原文 · 12 行)
frameworks-mobile/Input.html · basic
<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>
带前缀prefix

单位写在框内,避免占位符里重复出现(前缀对读屏隐藏,语义由 label 承担)。

查看代码(演示页原文 · 13 行)
frameworks-mobile/Input.html · prefix
<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>
可清除clearable=true

有值时右侧出现清除按钮;点它清空并把焦点交还输入框。

查看代码(演示页原文 · 14 行)
frameworks-mobile/Input.html · clearable
<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>
尺寸两档size=default|large

default 44px 用于表单内多字段;large 52px 用于一行一字段。

查看代码(演示页原文 · 21 行)
frameworks-mobile/Input.html · size
<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 组件状态

错误态状态 error

is-error 边框变错误色,aria-invalid 置位,错误说明在框外下方。

查看代码(演示页原文 · 14 行)
frameworks-mobile/Input.html · error
<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>
禁用disabled=true

整框置灰、输入区不可聚焦,读屏会跳过。

查看代码(演示页原文 · 12 行)
frameworks-mobile/Input.html · disabled
<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

名称类型默认值说明必传
valuestring''受控值;有值时右侧才出现清除按钮(规格 §27.5)N
size'default' | 'large''default'变体 size:default 高 44px,large 高 52px(规格 §27.3)N
placeholderstring''占位文字;它不算标签,标签由 label 给出(规格 §27.6)N
prefixstring''可选前缀,放单位或符号(规格 §27.2 prefix)N
clearablebooleanfalse变体 clearable:有值时显示清除按钮(规格 §27.3)N
disabledbooleanfalse状态 disabled:置灰且不可聚焦(规格 §27.4)N
errorstring''状态 error:错误文案,非空时写 aria-invalid 并在框外下方显示(规格 §27.6)N
labelstring'输入框'无障碍名称,落到 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-heightvar(--kole-m-touch-target)默认高度(≥ 44px 触控最小边长)
--kole-m-input-duration150ms边框与聚焦外发光的过渡时长

何时使用

  • 在一行内收集单行文本(姓名、手机号、金额、验证码等)
  • 移动端输入框整行占满、高度不小于 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 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
sizedefault / large
default (由数据驱动,无专属类)
large .kole-m-input--large
clearablefalse / true
false (由数据驱动,无专属类)
true .kole-m-input__clear

代表变体

变体标签
size=default · clearable=false默认输入框(44px,一行一字段)
size=large · clearable=false大尺寸(52px,单字段成组)
size=default · clearable=true可清除(有值时出现清除按钮)

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。

--kole-m-font-size-body --kole-m-font-size-label --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-card-bg --kole-color-disabled-bg --kole-color-error --kole-color-focus-ring --kole-color-text-body --kole-color-text-disabled --kole-color-text-placeholder --kole-color-text-secondary --kole-ease-standard --kole-radius-base --kole-space-12 --kole-space-8 --kole-m-input-duration --kole-m-input-height

6 端源码

同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。

frameworks-mobile/Input.css · 纯样式(CSS) · 130 行
frameworks-mobile/Input.css
/* 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 行
frameworks-mobile/Input.html
<!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 行
frameworks-mobile/Input.jsx
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 行
frameworks-mobile/Input.vue2.vue
<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 行
frameworks-mobile/Input.vue3.vue
<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 行
frameworks-mobile/Input.uniapp.vue
<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": "金额/验证码等需要自定义键盘时,输入框配套数字键盘;键盘只回传按键,写入仍由宿主完成"
    }
  ]
}