移动端导航链接

链接Link

一段内联文字承载「跳转 / 打开下一级」或「触发一次轻量动作」

通用 规格 21 · 链接 Link 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-link.css">

<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->

演示

每个演示都是真实渲染:预览帧加载 frameworks-mobile/Link.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。

01 组件类型

语义色tone=brand | default | danger | success

默认品牌色;secondary 场景用 default 继承父级文字色;破坏性操作用 danger。

查看代码(演示页原文 · 9 行)
frameworks-mobile/Link.html · tone
<section class="demo-block" data-demo="tone">
  <p class="demo-label">语义色(tone=brand 默认 / default 继承父级 / danger 破坏性 / success)</p>
  <div class="demo-box" data-assert="link-tone">
    <a class="kole-m-link" href="#link-tone">查看订单详情</a>
    <a class="kole-m-link kole-m-link--default" href="#link-tone">稍后再说</a>
    <a class="kole-m-link kole-m-link--danger" href="#link-tone">删除记录</a>
    <a class="kole-m-link kole-m-link--success" href="#link-tone">已完成认证</a>
  </div>
</section>
正文段落内链接underline=true

段落里的链接必须带下划线 —— 只靠颜色区分会让色觉障碍用户看不出来。

查看代码(演示页原文 · 11 行)
frameworks-mobile/Link.html · underline
<section class="demo-block" data-demo="underline">
  <p class="demo-label">下划线(underline=true:正文段落内必须能一眼分辨出链接)</p>
  <div class="demo-box" data-assert="link-underline">
    <p class="demo-para">
      下单即表示同意
      <a class="kole-m-link kole-m-link--underline" href="#link-underline">《用户服务协议》</a>
      与
      <a class="kole-m-link kole-m-link--underline" href="#link-underline">《隐私政策》</a>。
    </p>
  </div>
</section>

02 组件状态

禁用disabled=true

置灰且不响应;同时去掉 href 并移出 tab 序列,否则读屏仍会把它当可点项。

查看代码(演示页原文 · 6 行)
frameworks-mobile/Link.html · disabled
<section class="demo-block" data-demo="disabled">
  <p class="demo-label">禁用(disabled:置灰且不响应;同时不渲染 href,否则读屏仍会把它当可点项)</p>
  <div class="demo-box" data-assert="link-disabled">
    <button class="kole-m-link kole-m-link--default is-disabled" type="button" disabled aria-disabled="true">退款申请(已超过 7 天)</button>
  </div>
</section>
整行链接block=true

列表里「查看全部」这类入口用 block,整行都可点(热区 44px),不必精准点文字。

查看代码(演示页原文 · 6 行)
frameworks-mobile/Link.html · block
<section class="demo-block" data-demo="block">
  <p class="demo-label">整行链接(block=true:撑满容器,热区 44px,列表里整行可点)</p>
  <div class="demo-box" data-assert="link-block">
    <a class="kole-m-link is-block" href="#link-block">查看全部 12 条评价</a>
  </div>
</section>
不跳转的链接href=''

无 href 时渲染 button:只回传事件,用于打开弹窗或展开区块,不做页面跳转。

查看代码(演示页原文 · 7 行)
frameworks-mobile/Link.html · event
<section class="demo-block" data-demo="event">
  <p class="demo-label">不跳转的链接(无 href 时渲染 button:只回传事件,用于弹窗 / 展开)</p>
  <div class="demo-box" data-assert="link-event">
    <button class="kole-m-link" type="button" id="link-event-btn"
            data-behavior="click-sets-attr:#link-event-btn|data-clicked|true">查看物流</button>
  </div>
</section>
带图标tone=brand + 图标

尾部图标继承链接颜色,与文字间距 4px;图标自身不是热区。

查看代码(演示页原文 · 9 行)
frameworks-mobile/Link.html · with-icon
<section class="demo-block" data-demo="with-icon">
  <p class="demo-label">带图标(图标继承链接颜色,与文字间距 4px;图标自身不是热区)</p>
  <div class="demo-box" data-assert="link-with-icon">
    <a class="kole-m-link" href="#link-with-icon">
      <span class="kole-m-link__label">去填写地址</span>
      <span class="demo-arrow" aria-hidden="true">›</span>
    </a>
  </div>
</section>

API

props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs

Props

名称类型默认值说明必传
tone'brand' | 'default' | 'danger' | 'success''brand'变体 tone:brand 默认 / default 继承父级文字色(规格 §21.3)N
underlinebooleanfalse变体 underline:常驻下划线,正文段落内必须开启(规格 §21.3/§21.6)N
disabledbooleanfalse置灰且不响应点击,并移出 tab 序列(规格 §21.4/§21.6)N
hrefstring''跳转地址;有值时渲染原生 a,禁用时不渲染该属性(规格 §21.2)N
textstring''纯文字快捷入口,与默认插槽二选一(规格 §21.2)N
blockbooleanfalse变体 block:撑满容器、整行可点(规格 §21.3)N

「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。

事件

名称参数说明
click—点击时触发(disabled 时不触发;有 href 时导航由浏览器完成)

插槽

名称说明
default链接文字,可含图标(规格 §21.2 label)

CSS 变量

组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。

名称默认值说明
本组件没有组件级 CSS 变量

何时使用

  • 一段内联文字承载「跳转 / 打开下一级」或「触发一次轻量动作」
  • 点击热区高度不小于 44px,文字可短但热区不缩水(内边距撑开)
  • 按下反馈为颜色变化,不做位移缩放(避免长按抖动)
  • 相邻链接之间至少留 8px 间距,防止误触(同时给出 16px 的更稳选择)
  • 禁用链接点击不触发事件,也不跳转

交互与触控

  • 点击热区高度不小于 44px,文字可短但热区不缩水(内边距撑开)
  • 按下反馈为颜色变化,不做位移缩放(避免长按抖动)
  • 相邻链接之间至少留 8px 间距,防止误触(同时给出 16px 的更稳选择)
  • 禁用链接点击不触发事件,也不跳转

无障碍

  • 有 href 用原生 a(可聚焦、可长按复制、读屏报「链接」);无 href 用原生 button
  • 禁用链接不能用 href,且要 aria-disabled="true" + tabindex="-1"(移出 tab 序列)
  • 链接文字必须自解释,不要出现孤立的「点击这里」(读屏会脱离上下文朗读)
  • 颜色不是唯一线索:正文段落内的链接必须带下划线或图标,避免色觉障碍用户无法识别

从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。

组件何时用它而不是本组件
按钮Button触发一个明确动作(提交、确认)用按钮;链接只用于跳转或极轻的次要动作
单元格Cell整行进入下级、且行内有多段信息时用单元格;行内文字短语才用链接
图标Icon需要给链接配图标时用它渲染字形;图标不承担点击

规格未定 / 禁止发明

类别条目
禁止发明外跳协议处理(tel: / mailto: / 唤起 App)
禁止发明已访问状态的样式
禁止发明链接的埋点 / 统计
规格未定一屏内链接的最大推荐数量
规格未定链接与相邻文字的推荐最小间距(同时给出 8px 与 16px 两档)

结构(anatomy)

字段说明
link根元素,有 href 时是原生 a,无 href 时是原生 button(只回传事件)
label链接文字,单行不换行(超长由宿主截断)
icon可选尾部图标,继承链接颜色,与文字间距 4px
href跳转地址;禁用时不渲染该属性(否则仍可被打开)
text纯文字快捷入口,与默认插槽二选一

变体维度与类名映射

类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
tonebrand / default / danger / success
brand (由数据驱动,无专属类)
default .kole-m-link--default
danger .kole-m-link--danger
success .kole-m-link--success
underlinefalse / true
false (由数据驱动,无专属类)
true .kole-m-link--underline
blockfalse / true
false (由数据驱动,无专属类)
true .is-block

代表变体

变体标签
tone=brand · underline=false品牌色链接(默认,行内主入口)
tone=default · underline=false次要链接(继承父级文字色)
tone=danger · underline=false破坏性操作链接
tone=brand · underline=true正文段落内链接(必须带下划线)

用到的令牌

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

--kole-m-font-size-body --kole-m-gutter --kole-m-touch-target --kole-color-brand --kole-color-brand-active --kole-color-brand-bg --kole-color-disabled-bg --kole-color-error --kole-color-focus-ring --kole-color-success --kole-color-text-disabled --kole-duration-fast --kole-ease-standard --kole-radius-small --kole-space-4

6 端源码

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

frameworks-mobile/Link.css · 纯样式(CSS) · 61 行
frameworks-mobile/Link.css
/* Kole UI Mobile · Link 样式 — 对齐 .design_library/kole-ui-mobile/spec/移动端规格.md §21
   文字链接:默认**不下划线**(移动端正文里满屏下划线噪声大),靠颜色区分;
   需要「明确是链接」的场景(正文段落内、条款页)再加下划线。
   与桌面端的差别:没有 hover,按下反馈用 :active 变色,不做透明度位移。 */

.kole-m-link {
  display: inline-flex;
  align-items: center;
  gap: var(--kole-space-4);
  box-sizing: border-box;
  min-height: var(--kole-m-touch-target);
  padding: 0 var(--kole-space-4);
  border: 0;
  border-radius: var(--kole-radius-small);
  background: transparent;
  color: var(--kole-color-brand);
  font-family: inherit;
  font-size: var(--kole-m-font-size-body);
  line-height: 1.4;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  transition: color var(--kole-duration-fast) var(--kole-ease-standard),
              background-color var(--kole-duration-fast) var(--kole-ease-standard);
}

/* 变体 tone:默认品牌色(可点);default 继承父级文字色(次要链接);danger 用于破坏性操作 */
.kole-m-link--default { color: inherit; }
.kole-m-link--danger { color: var(--kole-color-error); }
.kole-m-link--success { color: var(--kole-color-success); }

/* 变体 underline=true:常驻下划线(正文段落内 / 条款页里必须能一眼分辨) */
.kole-m-link--underline { text-decoration: underline; text-underline-offset: 3px; }

/* 按下反馈:变色 + 极浅底色;不做位移缩放 */
.kole-m-link:active { color: var(--kole-color-brand-active); background: var(--kole-color-brand-bg); }
.kole-m-link--danger:active { background: var(--kole-color-disabled-bg); }

.kole-m-link__label { white-space: nowrap; }

/* 变体 tone=block:撑满容器(列表里整行都可点) */
.kole-m-link.is-block { display: flex; width: 100%; justify-content: flex-start; padding: 0 var(--kole-m-gutter); }

/* 状态 disabled:置灰且不响应;保留 tabindex=-1 以便读屏跳过 */
.kole-m-link.is-disabled,
.kole-m-link:disabled {
  color: var(--kole-color-text-disabled);
  text-decoration-color: var(--kole-color-text-disabled);
  cursor: not-allowed;
}
.kole-m-link.is-disabled:active,
.kole-m-link:disabled:active { background: transparent; }

.kole-m-link:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: 2px;
}
frameworks-mobile/Link.html · H5 原生(无框架) · 111 行
frameworks-mobile/Link.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 · Link(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Link.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-space-4) 0; background: var(--kole-color-card-bg);
    border-block: 1px solid var(--kole-color-border); }
  .demo-para { margin: 0; padding: 0 var(--kole-m-gutter); font-size: var(--kole-m-font-size-body); line-height: 1.8; }
  /* 演示用箭头字形:仅本页样式,不引其它组件类(组件间样式不互相依赖) */
  .demo-arrow { display: inline-flex; align-items: center; padding-left: var(--kole-space-4);
    color: var(--kole-color-text-placeholder); line-height: 1; }
  .demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="tone">
    <p class="demo-label">语义色(tone=brand 默认 / default 继承父级 / danger 破坏性 / success)</p>
    <div class="demo-box" data-assert="link-tone">
      <a class="kole-m-link" href="#link-tone">查看订单详情</a>
      <a class="kole-m-link kole-m-link--default" href="#link-tone">稍后再说</a>
      <a class="kole-m-link kole-m-link--danger" href="#link-tone">删除记录</a>
      <a class="kole-m-link kole-m-link--success" href="#link-tone">已完成认证</a>
    </div>
  </section>

  <section class="demo-block" data-demo="underline">
    <p class="demo-label">下划线(underline=true:正文段落内必须能一眼分辨出链接)</p>
    <div class="demo-box" data-assert="link-underline">
      <p class="demo-para">
        下单即表示同意
        <a class="kole-m-link kole-m-link--underline" href="#link-underline">《用户服务协议》</a>
        与
        <a class="kole-m-link kole-m-link--underline" href="#link-underline">《隐私政策》</a>。
      </p>
    </div>
  </section>

  <section class="demo-block" data-demo="disabled">
    <p class="demo-label">禁用(disabled:置灰且不响应;同时不渲染 href,否则读屏仍会把它当可点项)</p>
    <div class="demo-box" data-assert="link-disabled">
      <button class="kole-m-link kole-m-link--default is-disabled" type="button" disabled aria-disabled="true">退款申请(已超过 7 天)</button>
    </div>
  </section>

  <section class="demo-block" data-demo="block">
    <p class="demo-label">整行链接(block=true:撑满容器,热区 44px,列表里整行可点)</p>
    <div class="demo-box" data-assert="link-block">
      <a class="kole-m-link is-block" href="#link-block">查看全部 12 条评价</a>
    </div>
  </section>

  <section class="demo-block" data-demo="event">
    <p class="demo-label">不跳转的链接(无 href 时渲染 button:只回传事件,用于弹窗 / 展开)</p>
    <div class="demo-box" data-assert="link-event">
      <button class="kole-m-link" type="button" id="link-event-btn"
              data-behavior="click-sets-attr:#link-event-btn|data-clicked|true">查看物流</button>
    </div>
  </section>

  <section class="demo-block" data-demo="with-icon">
    <p class="demo-label">带图标(图标继承链接颜色,与文字间距 4px;图标自身不是热区)</p>
    <div class="demo-box" data-assert="link-with-icon">
      <a class="kole-m-link" href="#link-with-icon">
        <span class="kole-m-link__label">去填写地址</span>
        <span class="demo-arrow" aria-hidden="true">›</span>
      </a>
    </div>
  </section>
</div>
<script>
  /* 演示页脚本:不跳转的链接被点击后打标记(真实业务里是打开弹窗 / 展开区块) */
  (function () {
    var btn = document.getElementById('link-event-btn');
    if (!btn) return;
    btn.addEventListener('click', function () {
      btn.setAttribute('data-clicked', 'true');
    });
  })();
</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/Link.jsx · React · 50 行
frameworks-mobile/Link.jsx
import React from 'react';
import './Link.css';

/* 链接(移动端)— 规格 §21;有 href 时渲染原生 <a>(可键盘聚焦、可长按复制),
   无 href 时渲染 <button type="button">(只触发事件、不跳转)。 */
export default function Link({
  tone = 'brand',
  underline = false,
  disabled = false,
  href = '',
  text = '',
  block = false,
  onClick,
  children = null,
}) {
  const cls =
    'kole-m-link' +
    (tone === 'default' ? ' kole-m-link--default' : '') +
    (tone === 'danger' ? ' kole-m-link--danger' : '') +
    (tone === 'success' ? ' kole-m-link--success' : '') +
    (underline ? ' kole-m-link--underline' : '') +
    (block ? ' is-block' : '') +
    (disabled ? ' is-disabled' : '');

  /* 规格 §21.6:禁用链接不能用 href(否则仍可被打开),且要移出 tab 序列 */
  const content = <span className="kole-m-link__label">{text || children}</span>;

  if (href && !disabled) {
    return (
      <a className={cls} href={href} onClick={onClick ? () => onClick() : undefined}>
        {content}
      </a>
    );
  }
  return (
    <button
      className={cls}
      type="button"
      disabled={disabled}
      aria-disabled={disabled ? 'true' : 'false'}
      onClick={() => {
        if (disabled) return;
        if (onClick) onClick();
      }}
    >
      {content}
    </button>
  );
}
frameworks-mobile/Link.vue2.vue · Vue 2 · 49 行
frameworks-mobile/Link.vue2.vue
<template>
  <a v-if="href && !disabled" class="kole-m-link" :class="linkClass" :href="href" @click="$emit('click')">
    <span class="kole-m-link__label"><slot>{{ text }}</slot></span>
  </a>
  <button v-else class="kole-m-link" :class="linkClass" type="button" :disabled="disabled"
          :aria-disabled="disabled ? 'true' : 'false'" @click="onActivate">
    <span class="kole-m-link__label"><slot>{{ text }}</slot></span>
  </button>
</template>

<script>
var TONE_CLASS = {
  default: 'kole-m-link--default',
  danger: 'kole-m-link--danger',
  success: 'kole-m-link--success'
};

export default {
  name: 'KoleMLink',
  props: {
    tone: { type: String, default: 'brand' },
    underline: { type: Boolean, default: false },
    disabled: { type: Boolean, default: false },
    href: { type: String, default: '' },
    text: { type: String, default: '' },
    block: { type: Boolean, default: false }
  },
  computed: {
    linkClass: function () {
      return [
        TONE_CLASS[this.tone],
        this.underline ? 'kole-m-link--underline' : '',
        this.block ? 'is-block' : '',
        this.disabled ? 'is-disabled' : ''
      ].filter(Boolean);
    }
  },
  methods: {
    onActivate: function () {
      /* 规格 §21.6:禁用链接不触发事件 */
      if (this.disabled) return;
      this.$emit('click');
    }
  }
};
</script>

<style src="./Link.css"></style>
frameworks-mobile/Link.vue3.vue · Vue 3 · 42 行
frameworks-mobile/Link.vue3.vue
<template>
  <a v-if="href && !disabled" class="kole-m-link" :class="linkClass" :href="href" @click="onActivate">
    <span class="kole-m-link__label"><slot>{{ text }}</slot></span>
  </a>
  <button v-else class="kole-m-link" :class="linkClass" type="button" :disabled="disabled"
          :aria-disabled="disabled ? 'true' : 'false'" @click="onActivate">
    <span class="kole-m-link__label"><slot>{{ text }}</slot></span>
  </button>
</template>

<script setup>
import { computed } from 'vue';

const TONE_CLASS = {
  default: 'kole-m-link--default',
  danger: 'kole-m-link--danger',
  success: 'kole-m-link--success'
};

const props = defineProps({
  tone: { type: String, default: 'brand' },
  underline: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  href: { type: String, default: '' },
  text: { type: String, default: '' },
  block: { type: Boolean, default: false }
});
const emit = defineEmits(['click']);

const linkClass = computed(() =>
  [TONE_CLASS[props.tone], props.underline ? 'kole-m-link--underline' : '', props.block ? 'is-block' : '', props.disabled ? 'is-disabled' : ''].filter(Boolean)
);

function onActivate() {
  /* 规格 §21.6:禁用链接不触发事件(渲染层已用 disabled,这里再拦一次) */
  if (props.disabled) return;
  emit('click');
}
</script>

<style src="./Link.css"></style>
frameworks-mobile/Link.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 78 行
frameworks-mobile/Link.uniapp.vue
<template>
  <view
    class="kole-m-link"
    :class="linkClass"
    role="link"
    :aria-disabled="disabled ? 'true' : 'false'"
    :data-href="href"
    @tap="onActivate"
  >
    <text class="kole-m-link__label"><slot>{{ text }}</slot></text>
  </view>
</template>

<script setup>
/* uni-app 端 · 链接(移动端)— 规格 §21
   跨端差异:小程序/App 无 <a>,用 view + role="link";跳转由宿主读 data-href 决定
   (组件只 emit click,不自己做路由)。点击用 @tap;尺寸用 rpx(88rpx = 44px @375pt)。 */
import { computed } from 'vue';

const TONE_CLASS = {
  default: 'kole-m-link--default',
  danger: 'kole-m-link--danger',
  success: 'kole-m-link--success'
};

const props = defineProps({
  tone: { type: String, default: 'brand' },
  underline: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  href: { type: String, default: '' },
  text: { type: String, default: '' },
  block: { type: Boolean, default: false }
});
const emit = defineEmits(['click']);

const linkClass = computed(() =>
  [
    TONE_CLASS[props.tone],
    props.underline ? 'kole-m-link--underline' : '',
    props.block ? 'is-block' : '',
    props.disabled ? 'is-disabled' : ''
  ].filter(Boolean)
);

function onActivate() {
  /* 规格 §21.6:禁用链接不触发事件 */
  if (props.disabled) return;
  emit('click');
}
</script>

<style>
.kole-m-link {
  display: flex;
  align-items: center;
  box-sizing: border-box;
  min-height: 88rpx;
  padding: 0 8rpx;
  border-radius: var(--kole-radius-small);
  background-color: transparent;
  color: var(--kole-color-brand);
  font-size: var(--kole-m-font-size-body);
  text-decoration: none;
}

.kole-m-link--default { color: inherit; }
.kole-m-link--danger { color: var(--kole-color-error); }
.kole-m-link--success { color: var(--kole-color-success); }

.kole-m-link--underline { text-decoration: underline; }

.kole-m-link.is-block { width: 100%; padding: 0 32rpx; }

.kole-m-link.is-disabled { color: var(--kole-color-text-disabled); }

.kole-m-link__label { line-height: 1.4; }
</style>

测试与回归

断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。

断言 17 条 · 全部通过 报告 2026-09-20 16:50:33

复现命令
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-link.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "specSection": "21 · 链接 Link",
  "confidence": "high",
  "slug": "mobile-link",
  "name": "链接 Link",
  "semanticTypeCandidates": [
    "link",
    "anchor"
  ],
  "variantDimensions": [
    {
      "name": "tone",
      "values": [
        "brand",
        "default",
        "danger",
        "success"
      ]
    },
    {
      "name": "underline",
      "values": [
        "false",
        "true"
      ]
    },
    {
      "name": "block",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "tone": "brand",
      "underline": "false",
      "label": "品牌色链接(默认,行内主入口)"
    },
    {
      "tone": "default",
      "underline": "false",
      "label": "次要链接(继承父级文字色)"
    },
    {
      "tone": "danger",
      "underline": "false",
      "label": "破坏性操作链接"
    },
    {
      "tone": "brand",
      "underline": "true",
      "label": "正文段落内链接(必须带下划线)"
    }
  ],
  "anatomy": {
    "link": "根元素,有 href 时是原生 a,无 href 时是原生 button(只回传事件)",
    "label": "链接文字,单行不换行(超长由宿主截断)",
    "icon": "可选尾部图标,继承链接颜色,与文字间距 4px",
    "href": "跳转地址;禁用时不渲染该属性(否则仍可被打开)",
    "text": "纯文字快捷入口,与默认插槽二选一"
  },
  "structurePatterns": {
    "tone": "brand 默认 / default 继承父级 / danger 破坏性 / success 完成态",
    "underline": "false 默认(靠颜色区分)/ true 常驻下划线(正文段落内)",
    "block": "false 内联 / true 撑满容器、整行可点"
  },
  "usageHints": [
    "一段内联文字承载「跳转 / 打开下一级」或「触发一次轻量动作」",
    "点击热区高度不小于 44px,文字可短但热区不缩水(内边距撑开)",
    "按下反馈为颜色变化,不做位移缩放(避免长按抖动)",
    "相邻链接之间至少留 8px 间距,防止误触(同时给出 16px 的更稳选择)",
    "禁用链接点击不触发事件,也不跳转"
  ],
  "doNotInvent": [
    "外跳协议处理(tel: / mailto: / 唤起 App)",
    "已访问状态的样式",
    "链接的埋点 / 统计"
  ],
  "unknowns": [
    "一屏内链接的最大推荐数量",
    "链接与相邻文字的推荐最小间距(同时给出 8px 与 16px 两档)"
  ],
  "interaction": [
    "点击热区高度不小于 44px,文字可短但热区不缩水(内边距撑开)",
    "按下反馈为颜色变化,不做位移缩放(避免长按抖动)",
    "相邻链接之间至少留 8px 间距,防止误触(同时给出 16px 的更稳选择)",
    "禁用链接点击不触发事件,也不跳转"
  ],
  "accessibility": [
    "有 href 用原生 a(可聚焦、可长按复制、读屏报「链接」);无 href 用原生 button",
    "禁用链接不能用 href,且要 aria-disabled=\"true\" + tabindex=\"-1\"(移出 tab 序列)",
    "链接文字必须自解释,不要出现孤立的「点击这里」(读屏会脱离上下文朗读)",
    "颜色不是唯一线索:正文段落内的链接必须带下划线或图标,避免色觉障碍用户无法识别"
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "tone",
        "type": "'brand' | 'default' | 'danger' | 'success'",
        "default": "'brand'",
        "desc": "变体 tone:brand 默认 / default 继承父级文字色(规格 §21.3)",
        "required": false
      },
      {
        "name": "underline",
        "type": "boolean",
        "default": "false",
        "desc": "变体 underline:常驻下划线,正文段落内必须开启(规格 §21.3/§21.6)",
        "required": false
      },
      {
        "name": "disabled",
        "type": "boolean",
        "default": "false",
        "desc": "置灰且不响应点击,并移出 tab 序列(规格 §21.4/§21.6)",
        "required": false
      },
      {
        "name": "href",
        "type": "string",
        "default": "''",
        "desc": "跳转地址;有值时渲染原生 a,禁用时不渲染该属性(规格 §21.2)",
        "required": false
      },
      {
        "name": "text",
        "type": "string",
        "default": "''",
        "desc": "纯文字快捷入口,与默认插槽二选一(规格 §21.2)",
        "required": false
      },
      {
        "name": "block",
        "type": "boolean",
        "default": "false",
        "desc": "变体 block:撑满容器、整行可点(规格 §21.3)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "click",
        "params": "—",
        "desc": "点击时触发(disabled 时不触发;有 href 时导航由浏览器完成)"
      }
    ],
    "slots": [
      {
        "name": "default",
        "desc": "链接文字,可含图标(规格 §21.2 label)"
      }
    ]
  },
  "variantClasses": {
    "tone": {
      "brand": [],
      "default": [
        ".kole-m-link--default"
      ],
      "danger": [
        ".kole-m-link--danger"
      ],
      "success": [
        ".kole-m-link--success"
      ]
    },
    "underline": {
      "false": [],
      "true": [
        ".kole-m-link--underline"
      ]
    },
    "block": {
      "false": [],
      "true": [
        ".is-block"
      ]
    }
  },
  "demos": [
    {
      "id": "tone",
      "group": "01 组件类型",
      "title": "语义色",
      "desc": "默认品牌色;secondary 场景用 default 继承父级文字色;破坏性操作用 danger。",
      "variant": "tone=brand | default | danger | success"
    },
    {
      "id": "underline",
      "group": "01 组件类型",
      "title": "正文段落内链接",
      "desc": "段落里的链接必须带下划线 —— 只靠颜色区分会让色觉障碍用户看不出来。",
      "variant": "underline=true"
    },
    {
      "id": "disabled",
      "group": "02 组件状态",
      "title": "禁用",
      "desc": "置灰且不响应;同时去掉 href 并移出 tab 序列,否则读屏仍会把它当可点项。",
      "variant": "disabled=true"
    },
    {
      "id": "block",
      "group": "02 组件状态",
      "title": "整行链接",
      "desc": "列表里「查看全部」这类入口用 block,整行都可点(热区 44px),不必精准点文字。",
      "variant": "block=true"
    },
    {
      "id": "event",
      "group": "02 组件状态",
      "title": "不跳转的链接",
      "desc": "无 href 时渲染 button:只回传事件,用于打开弹窗或展开区块,不做页面跳转。",
      "variant": "href=''"
    },
    {
      "id": "with-icon",
      "group": "02 组件状态",
      "title": "带图标",
      "desc": "尾部图标继承链接颜色,与文字间距 4px;图标自身不是热区。",
      "variant": "tone=brand + 图标"
    }
  ],
  "related": [
    {
      "slug": "mobile-button",
      "why": "触发一个明确动作(提交、确认)用按钮;链接只用于跳转或极轻的次要动作"
    },
    {
      "slug": "cell",
      "why": "整行进入下级、且行内有多段信息时用单元格;行内文字短语才用链接"
    },
    {
      "slug": "mobile-icon",
      "why": "需要给链接配图标时用它渲染字形;图标不承担点击"
    }
  ]
}