Regression / regression (push) Canceled after 0s
安全:CHANGELOG.md 是仓库内部变更流水(含服务器目录、镜像回滚标签、内网网段、
部署时序、AI 工作流用语),此前被 build-site.ps1 / precompute.mjs / build-mobile.mjs
原样注入站点,而站点是公网可下载的静态文件 —— 抓一次 /site/m/changelog.html
即可拿到内网地址段与服务器目录布局。
- 新增 tools/lib/redact-publish.mjs(零依赖,构建期过滤,不改 CHANGELOG.md,
内部可追溯性完整保留)
- 接入 precompute.mjs(PC data.json/changelog.json)与 build-mobile.mjs
(移动端更新日志页);build-site.ps1 不碰(ASCII-only 铁律)
- 只处理 changelog 字段:components[].sources 是规范实现源码,逐字保真
(详情页代码区主动高亮注释,剥注释会破坏该功能)
- 实测消除:/opt/aurora-admin.prev-*、kole-ui-showcase:pre-*、docker compose、
192.168.5.7、16 位产物指纹、1531/1531、并发会话/本会话/派子 agent
- settings.html 演示占位 IP 192.168.5.0/24(= 真实网段)改为 RFC 5737 的 192.0.2.0/24
品牌:Kole Cup 饮料杯标记定稿(几何 K → 圆角杯盖 + 杯身负空间 K,无吸管),
brand-mark.json 升 schemaVersion 3(paths 支持 { d, evenodd }),
verify:brand 增至 25 条(新增 B9b:负空间必须带 fill-rule)。
验证:发布集 2465 文件全量扫描 0 泄露;PC 回归 100%(1464/1464) ·
移动端 100%(807/807);门禁品牌 25 / 隔离 31 / 移动文档 12 / 示例 9 / 版本 40 / i18n 17 全绿;
已按 AGENTS §九 发布公网,2461/2461 逐字节一致,五项验收全过。
已知未处理(既有缺口,ROADMAP S7-P28 已记录):data.mobile.json 的
meta.generated 为墙上时钟,会让 CI 的「生成物可复现」断言在重跑构建后永远非空;
该 CI 流水线本身亦从未通过(无 runner)。
2368 lines
124 KiB
HTML
2368 lines
124 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<script>
|
||
(function () {
|
||
var m = 'auto';
|
||
try { var v = localStorage.getItem('kole-mode'); if (v === 'light' || v === 'dark' || v === 'auto') m = v; } catch (e) {}
|
||
var dark = m === 'dark' || (m === 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||
document.documentElement.classList.toggle('kole-dark', !!dark);
|
||
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
|
||
})();
|
||
</script>
|
||
<link rel="icon" href="../../assets/kole-mark.svg">
|
||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||
<meta name="theme-color" content="#1C1F26" media="(prefers-color-scheme: dark)">
|
||
<title>表格 Table — Kole UI Mobile</title>
|
||
<meta name="description" content="表格 Table:移动端组件(6 端实现:CSS / H5 原生 / React / Vue 2 / Vue 3 / uni-app)。与 PC 端物理隔离:类名 kole-m-、令牌 --kole-m-。规格来源:移动端规格 47 · 表格 Table。">
|
||
<link rel="stylesheet" href="../../../.design_library/kole-ui-mobile/colors_and_type.css">
|
||
<link rel="stylesheet" href="../style.css">
|
||
</head>
|
||
<body>
|
||
<header class="m-top">
|
||
<div class="m-top-inner">
|
||
<a class="m-logo" href="../index.html">
|
||
<span class="m-logo-mark" data-brand-mark="mono" aria-hidden="true"></span>
|
||
<span class="m-logo-text">Kole UI<em>移动端</em></span>
|
||
</a>
|
||
<div class="m-ver-picker" id="m-ver-picker">
|
||
<button id="m-ver-trigger" class="m-ver-trigger" type="button" data-single="1" aria-haspopup="listbox" aria-expanded="false" aria-label="选择版本" title="文档版本(清单来自部署根的 versions.json)">
|
||
<span id="m-ver">v1.0.0</span>
|
||
<svg class="m-ver-caret" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
||
</button>
|
||
<div id="m-ver-menu" class="m-ver-menu hidden" role="listbox" aria-label="文档版本">
|
||
<div class="m-ver-menu-head" id="m-ver-head">文档版本</div>
|
||
</div>
|
||
</div>
|
||
<nav class="m-nav"> <a href="../index.html" class="active" aria-current="page">组件总览</a>
|
||
<a href="../guide.html">快速开始</a>
|
||
<a href="../design.html">设计规范</a>
|
||
<a href="../faq.html">常见问题</a>
|
||
<a href="../changelog.html">更新日志</a></nav>
|
||
<div class="m-mode">
|
||
<button id="m-mode-trigger" class="m-mode-trigger" type="button" aria-haspopup="listbox" aria-expanded="false" aria-label="主题模式" title="主题模式">
|
||
<svg class="m-mode-icon" id="m-icon-moon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||
<svg class="m-mode-icon" id="m-icon-sun" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
||
<svg class="m-mode-icon" id="m-icon-auto" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none"><rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||
<span id="m-mode-cur">自动模式</span>
|
||
<svg class="m-mode-caret" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
||
</button>
|
||
<div id="m-mode-menu" class="m-menu hidden" role="listbox" aria-label="主题模式">
|
||
<div class="m-menu-head">主题模式</div>
|
||
<button class="m-menu-opt" type="button" role="option" data-mode="light" aria-selected="false"><span data-mode-label="light">日间模式</span></button>
|
||
<button class="m-menu-opt" type="button" role="option" data-mode="dark" aria-selected="false"><span data-mode-label="dark">夜间模式</span></button>
|
||
<button class="m-menu-opt" type="button" role="option" data-mode="auto" aria-selected="true"><span data-mode-label="auto">自动模式</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="m-body">
|
||
<details class="m-side" open>
|
||
<summary><span>移动端导航</span><span class="cur">表格</span></summary>
|
||
<nav>
|
||
<div class="m-side-section"><span>平台</span></div>
|
||
<a class="m-side-link m-side-platform" href="../../index.html"><span class="sp-top"><span class="sp-name">PC 端组件</span></span><span class="sp-sub">103 个中后台组件 · 5 端</span></a>
|
||
<a class="m-side-link m-side-platform" href="../index.html"><span class="sp-top"><span class="sp-name">移动端组件</span><span class="sp-badge">当前</span></span><span class="sp-sub">47 个组件 · 6 端 · 含 uni-app</span></a>
|
||
<div class="m-side-section"><span>开发指南</span></div>
|
||
<a class="m-side-link" href="../index.html">组件总览</a>
|
||
<a class="m-side-link" href="../guide.html">快速开始</a>
|
||
<a class="m-side-link" href="../design.html">设计规范</a>
|
||
<a class="m-side-link" href="../faq.html">常见问题</a>
|
||
<a class="m-side-link" href="../changelog.html">更新日志</a>
|
||
<a class="m-side-link" href="../platform.html">平台与端<em>移动端</em></a>
|
||
<a class="m-side-link" href="../../../tests/mobile/index.html" target="_blank">测试与回归<em>移动端</em></a>
|
||
<div class="m-side-section"><span>组件</span><span class="cnt">47</span></div>
|
||
<div class="m-side-group"><span>通用</span><span class="cnt">5</span></div>
|
||
<a class="m-side-link" href="mobile-button.html">按钮<em>Button</em></a>
|
||
<a class="m-side-link" href="mobile-divider.html">分割线<em>Divider</em></a>
|
||
<a class="m-side-link" href="mobile-icon.html">图标<em>Icon</em></a>
|
||
<a class="m-side-link" href="mobile-layout.html">布局<em>Layout</em></a>
|
||
<a class="m-side-link" href="mobile-link.html">链接<em>Link</em></a>
|
||
<div class="m-side-group"><span>导航</span><span class="cnt">6</span></div>
|
||
<a class="m-side-link" href="navbar.html">顶部导航栏<em>NavBar</em></a>
|
||
<a class="m-side-link" href="tabbar.html">底部标签栏<em>TabBar</em></a>
|
||
<a class="m-side-link" href="mobile-grid.html">宫格<em>Grid</em></a>
|
||
<a class="m-side-link" href="mobile-steps.html">步骤条<em>Steps</em></a>
|
||
<a class="m-side-link" href="mobile-segmented.html">分段器<em>Segmented</em></a>
|
||
<a class="m-side-link" href="mobile-sticky.html">吸顶容器<em>Sticky</em></a>
|
||
<div class="m-side-group"><span>数据录入</span><span class="cnt">16</span></div>
|
||
<a class="m-side-link" href="mobile-numberkeyboard.html">数字键盘<em>NumberKeyboard</em></a>
|
||
<a class="m-side-link" href="mobile-datepicker.html">日期选择器<em>DatePicker</em></a>
|
||
<a class="m-side-link" href="mobile-input.html">输入框<em>Input</em></a>
|
||
<a class="m-side-link" href="mobile-search.html">搜索框<em>Search</em></a>
|
||
<a class="m-side-link" href="mobile-stepper.html">步进器<em>Stepper</em></a>
|
||
<a class="m-side-link" href="mobile-switch.html">开关<em>Switch</em></a>
|
||
<a class="m-side-link" href="mobile-textarea.html">多行文本框<em>Textarea</em></a>
|
||
<a class="m-side-link" href="mobile-checkbox.html">多选框<em>Checkbox</em></a>
|
||
<a class="m-side-link" href="mobile-form.html">表单<em>Form</em></a>
|
||
<a class="m-side-link" href="mobile-radio.html">单选框<em>Radio</em></a>
|
||
<a class="m-side-link" href="mobile-rate.html">评分<em>Rate</em></a>
|
||
<a class="m-side-link" href="mobile-slider.html">滑动选择器<em>Slider</em></a>
|
||
<a class="m-side-link" href="mobile-cascader.html">级联选择器<em>Cascader</em></a>
|
||
<a class="m-side-link" href="mobile-colorpicker.html">颜色选择器<em>ColorPicker</em></a>
|
||
<a class="m-side-link" href="mobile-picker.html">选择器<em>Picker</em></a>
|
||
<a class="m-side-link" href="mobile-upload.html">上传<em>Upload</em></a>
|
||
<div class="m-side-group"><span>数据展示</span><span class="cnt">8</span></div>
|
||
<a class="m-side-link" href="cell.html">单元格<em>Cell</em></a>
|
||
<a class="m-side-link" href="mobile-badge.html">徽标<em>Badge</em></a>
|
||
<a class="m-side-link" href="mobile-tag.html">标签<em>Tag</em></a>
|
||
<a class="m-side-link" href="mobile-avatar.html">头像<em>Avatar</em></a>
|
||
<a class="m-side-link" href="mobile-list.html">列表<em>List</em></a>
|
||
<a class="m-side-link" href="mobile-collapse.html">折叠面板<em>Collapse</em></a>
|
||
<a class="m-side-link is-active" href="mobile-table.html" aria-current="page">表格<em>Table</em></a>
|
||
<a class="m-side-link" href="mobile-typography.html">排版<em>Typography</em></a>
|
||
<div class="m-side-group"><span>反馈</span><span class="cnt">12</span></div>
|
||
<a class="m-side-link" href="actionsheet.html">动作面板<em>ActionSheet</em></a>
|
||
<a class="m-side-link" href="pullrefresh.html">下拉刷新<em>PullRefresh</em></a>
|
||
<a class="m-side-link" href="swipecell.html">滑动单元格<em>SwipeCell</em></a>
|
||
<a class="m-side-link" href="mobile-popup.html">弹出层<em>Popup</em></a>
|
||
<a class="m-side-link" href="mobile-toast.html">轻提示<em>Toast</em></a>
|
||
<a class="m-side-link" href="mobile-dialog.html">对话框<em>Dialog</em></a>
|
||
<a class="m-side-link" href="mobile-noticebar.html">通知栏<em>NoticeBar</em></a>
|
||
<a class="m-side-link" href="mobile-loading.html">加载<em>Loading</em></a>
|
||
<a class="m-side-link" href="mobile-progress.html">进度条<em>Progress</em></a>
|
||
<a class="m-side-link" href="mobile-message.html">消息通知<em>Message</em></a>
|
||
<a class="m-side-link" href="mobile-overlay.html">遮罩层<em>Overlay</em></a>
|
||
<a class="m-side-link" href="mobile-popover.html">弹出气泡<em>Popover</em></a>
|
||
</nav>
|
||
</details>
|
||
<main class="m-main">
|
||
<section class="m-hero">
|
||
<h1>表格<span class="en" style="font-family: var(--kole-font-family-num); font-size: 14px; color: var(--kole-color-text-secondary); margin-left: 8px;">Table</span></h1>
|
||
<p>把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合</p>
|
||
<p style="margin-top: 8px;" class="m-chips">
|
||
<span class="m-chip m">数据展示</span>
|
||
<span class="m-chip">规格 47 · 表格 Table</span>
|
||
<span class="m-chip">6 端实现</span>
|
||
<span class="m-chip">触摸优先</span>
|
||
</p>
|
||
<div class="m-code" style="margin-top: var(--kole-space-12);">
|
||
<div class="m-code-head"><span>引入(H5 原生;其余 5 端见「快速开始」)</span><button class="m-copy" type="button" data-copy="code-import">复制</button></div>
|
||
<pre id="code-import"><code><!-- ① 令牌: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-table.css">
|
||
|
||
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) --></code></pre>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="demo">演示</h2>
|
||
<p class="m-sub">每个演示都是真实渲染:预览帧加载 <code>frameworks-mobile/Table.html?demo=<id></code>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 <a href="../../../frameworks-mobile/Table.html" target="_blank">演示页 ↗</a>。</p>
|
||
<h3 class="m-demo-group">01 组件类型</h3>
|
||
<div class="m-demo" id="demo-scroll">
|
||
<div class="m-demo-head"><span class="m-demo-title">横向滚动</span><span class="m-chip">mode=scroll</span></div>
|
||
<p class="m-demo-desc">列多时在表格自己的容器里左右滑,页面整体不横向溢出。</p>
|
||
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Table.html?demo=scroll" title="表格 Table · 横向滚动" loading="lazy"></iframe></div>
|
||
<details class="m-details"><summary>查看代码(演示页原文 · 50 行)</summary>
|
||
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Table.html · scroll</span><button class="m-copy" type="button" data-copy="code-demo-1">复制</button></div>
|
||
<pre id="code-demo-1"><code><section class="demo-block" data-demo="scroll">
|
||
<p class="demo-label">横向滚动(列多时在表格自己的容器里左右滑;页面整体不横向溢出)</p>
|
||
<div class="demo-pad" data-assert="table-scroll">
|
||
<p class="demo-hint">← 左右滑动查看全部 6 列 →</p>
|
||
<div class="kole-m-table kole-m-table--bordered" id="tb-scroll"
|
||
style="--kole-m-table-min-width: 560px" data-assert="table-scroll-wide">
|
||
<div class="kole-m-table__viewport" id="tb-scroll-viewport">
|
||
<table class="kole-m-table__inner">
|
||
<caption class="kole-m-table__caption">近 3 日订单</caption>
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">客户</th>
|
||
<th class="kole-m-table__head-cell" scope="col">状态</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
|
||
<th class="kole-m-table__head-cell" scope="col">下单时间</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--end" scope="col">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-001</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">王小明</td>
|
||
<td class="kole-m-table__cell">待发货</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 1,280.00</td>
|
||
<td class="kole-m-table__cell">09-20 09:12</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-002</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">李小红</td>
|
||
<td class="kole-m-table__cell">已发货</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 368.50</td>
|
||
<td class="kole-m-table__cell">09-20 10:35</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-003</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">赵大力</td>
|
||
<td class="kole-m-table__cell">已完成</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 5,600.00</td>
|
||
<td class="kole-m-table__cell">09-20 14:08</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section></code></pre></div>
|
||
</details>
|
||
</div>
|
||
<div class="m-demo" id="demo-card">
|
||
<div class="m-demo-head"><span class="m-demo-title">卡片模式</span><span class="m-chip">mode=card</span></div>
|
||
<p class="m-demo-desc">mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动。</p>
|
||
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Table.html?demo=card" title="表格 Table · 卡片模式" loading="lazy"></iframe></div>
|
||
<details class="m-details"><summary>查看代码(演示页原文 · 30 行)</summary>
|
||
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Table.html · card</span><button class="m-copy" type="button" data-copy="code-demo-2">复制</button></div>
|
||
<pre id="code-demo-2"><code><section class="demo-block" data-demo="card">
|
||
<p class="demo-label">卡片模式(mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动)</p>
|
||
<div class="demo-pad" data-assert="table-card">
|
||
<div class="kole-m-table kole-m-table--card" id="tb-card">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell" scope="col">客户</th>
|
||
<th class="kole-m-table__head-cell" scope="col">等级</th>
|
||
<th class="kole-m-table__head-cell" scope="col">本月消费</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell" data-label="客户">王小明</td>
|
||
<td class="kole-m-table__cell" data-label="等级">黄金</td>
|
||
<td class="kole-m-table__cell" data-label="本月消费">¥ 1,280.00</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell" data-label="客户">李小红</td>
|
||
<td class="kole-m-table__cell" data-label="等级">白银</td>
|
||
<td class="kole-m-table__cell" data-label="本月消费">¥ 368.50</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section></code></pre></div>
|
||
</details>
|
||
</div>
|
||
<h3 class="m-demo-group">02 组件状态</h3>
|
||
<div class="m-demo" id="demo-clickable">
|
||
<div class="m-demo-head"><span class="m-demo-title">表头固定与行点击</span><span class="m-chip">状态 selected | clickable</span></div>
|
||
<p class="m-demo-desc">点整行选中该行;数字列等宽右对齐便于按位对比,斑马纹便于横向对齐行。</p>
|
||
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Table.html?demo=clickable" title="表格 Table · 表头固定与行点击" loading="lazy"></iframe></div>
|
||
<details class="m-details"><summary>查看代码(演示页原文 · 41 行)</summary>
|
||
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Table.html · clickable</span><button class="m-copy" type="button" data-copy="code-demo-3">复制</button></div>
|
||
<pre id="code-demo-3"><code><section class="demo-block" data-demo="clickable">
|
||
<p class="demo-label">表头固定 + 列对齐 + 行点击(点整行选中该行;数字列等宽右对齐,便于按位对比)</p>
|
||
<div class="demo-pad" data-assert="table-clickable">
|
||
<div class="kole-m-table kole-m-table--clickable kole-m-table--stripe kole-m-table--bordered" id="tb-clickable"
|
||
data-selected="" data-assert="table-clickable-grid">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">商品</th>
|
||
<th class="kole-m-table__head-cell" scope="col">仓库</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">库存</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">单价</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row" data-row="sku-1" tabindex="0" role="row" aria-selected="false"
|
||
data-behavior="click-sets-attr:#tb-clickable|data-selected|sku-1">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">有机棉 T 恤</td>
|
||
<td class="kole-m-table__cell">杭州仓</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">1,240</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 89.00</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row" data-row="sku-2" tabindex="0" role="row" aria-selected="false">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">轻薄羽绒服</td>
|
||
<td class="kole-m-table__cell">广州仓</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">86</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 459.00</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row is-selected" data-row="sku-3" tabindex="0" role="row" aria-selected="true">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">帆布托特包</td>
|
||
<td class="kole-m-table__cell">杭州仓</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">512</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 129.00</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section></code></pre></div>
|
||
</details>
|
||
</div>
|
||
<div class="m-demo" id="demo-empty">
|
||
<div class="m-demo-head"><span class="m-demo-title">空态</span><span class="m-chip">状态 empty</span></div>
|
||
<p class="m-demo-desc">没有数据时显示占位文案,列宽语义不变,不会把容器撑宽。</p>
|
||
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Table.html?demo=empty" title="表格 Table · 空态" loading="lazy"></iframe></div>
|
||
<details class="m-details"><summary>查看代码(演示页原文 · 20 行)</summary>
|
||
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Table.html · empty</span><button class="m-copy" type="button" data-copy="code-demo-4">复制</button></div>
|
||
<pre id="code-demo-4"><code><section class="demo-block" data-demo="empty">
|
||
<p class="demo-label">空态(没有数据时显示占位文案;列宽语义不变,不会把容器撑宽)</p>
|
||
<div class="demo-pad" data-assert="table-empty">
|
||
<div class="kole-m-table" id="tb-empty" data-empty="true">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
|
||
<th class="kole-m-table__head-cell" scope="col">状态</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
<p class="kole-m-table__empty" role="status" aria-live="polite">当前筛选条件下没有订单</p>
|
||
</div>
|
||
</div>
|
||
</section></code></pre></div>
|
||
</details>
|
||
</div>
|
||
<div class="m-demo" id="demo-states">
|
||
<div class="m-demo-head"><span class="m-demo-title">禁用行与省略列</span><span class="m-chip">状态 disabled</span></div>
|
||
<p class="m-demo-desc">禁用行置灰不响应;长文本列单行省略,完整值在 title 里。</p>
|
||
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Table.html?demo=states" title="表格 Table · 禁用行与省略列" loading="lazy"></iframe></div>
|
||
<details class="m-details"><summary>查看代码(演示页原文 · 28 行)</summary>
|
||
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Table.html · states</span><button class="m-copy" type="button" data-copy="code-demo-5">复制</button></div>
|
||
<pre id="code-demo-5"><code><section class="demo-block" data-demo="states">
|
||
<p class="demo-label">禁用行与省略列(禁用行置灰不响应;长文本列单行省略,完整值在 title 里)</p>
|
||
<div class="demo-pad" data-assert="table-states">
|
||
<div class="kole-m-table kole-m-table--bordered" id="tb-states">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">收货地址</th>
|
||
<th class="kole-m-table__head-cell" scope="col">配送</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start kole-m-table__cell--ellipsis"
|
||
title="浙江省杭州市西湖区文三路 100 号 1 幢 1801 室">浙江省杭州市西湖区文三路 100 号 1 幢 1801 室</td>
|
||
<td class="kole-m-table__cell">顺丰</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row is-disabled" aria-disabled="true">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">广东省深圳市南山区科技园南区</td>
|
||
<td class="kole-m-table__cell">暂不可达</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section></code></pre></div>
|
||
</details>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="api">API</h2>
|
||
<p class="m-sub">props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs</p>
|
||
<h3 class="m-h3">Props</h3>
|
||
<table class="m-table">
|
||
<thead><tr><th>名称</th><th>类型</th><th>默认值</th><th>说明</th><th>必传</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>columns</code></td><td><code>Array<{ title: string, key?: string, align?: 'start'|'center'|'end', number?: boolean }></code></td><td><code>[]</code></td><td>列定义(宿主提供;number 是数字列的便捷写法,等价于 align=end + 等宽字)(规格 §47.3)</td><td>N</td></tr>
|
||
<tr><td><code>rows</code></td><td><code>Array<Array | { key?: string, selected?: boolean, disabled?: boolean }></code></td><td><code>[]</code></td><td>行数据,二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }(规格 §47.2)</td><td>N</td></tr>
|
||
<tr><td><code>mode</code></td><td><code>'scroll' | 'card'</code></td><td><code>'scroll'</code></td><td>变体 mode:scroll 横向滚动,card 每行摊成键值对(规格 §47.3)</td><td>N</td></tr>
|
||
<tr><td><code>size</code></td><td><code>'default' | 'compact'</code></td><td><code>'default'</code></td><td>变体 size:compact 收紧内边距与字号,行高仍不小于 44px(规格 §47.3)</td><td>N</td></tr>
|
||
<tr><td><code>stripe</code></td><td><code>boolean</code></td><td><code>false</code></td><td>变体 stripe:偶数行浅底,便于横向扫读时对齐行(规格 §47.3)</td><td>N</td></tr>
|
||
<tr><td><code>bordered</code></td><td><code>boolean</code></td><td><code>false</code></td><td>变体 bordered:单元格之间也画竖线(规格 §47.3)</td><td>N</td></tr>
|
||
<tr><td><code>clickable</code></td><td><code>boolean</code></td><td><code>false</code></td><td>状态 clickable:整行可点,按下反馈是整行背景变化(规格 §47.4)</td><td>N</td></tr>
|
||
<tr><td><code>caption</code></td><td><code>string</code></td><td><code>''</code></td><td>可选表格标题(说明这张表在讲什么)(规格 §47.2)</td><td>N</td></tr>
|
||
<tr><td><code>emptyText</code></td><td><code>string</code></td><td><code>'暂无数据'</code></td><td>状态 empty 的占位文案(规格 §47.4)</td><td>N</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="m-note">「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。</p>
|
||
<h3 class="m-h3">事件</h3>
|
||
<table class="m-table">
|
||
<thead><tr><th>名称</th><th>参数</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>select</code></td><td><code>index: number, row: any</code></td><td>点整行时触发,回传行下标与该行数据(clickable=true 时才有意义)(规格 §47.5)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<h3 class="m-h3">插槽</h3>
|
||
<table class="m-table">
|
||
<thead><tr><th>名称</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>default</code></td><td>自定义内容(追加在空态之后)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<h3 class="m-h3">CSS 变量</h3>
|
||
<p class="m-sub">组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。</p>
|
||
<table class="m-table">
|
||
<thead><tr><th>名称</th><th>默认值</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>--kole-m-table-row-height</code></td><td><code>var(--kole-m-touch-target)</code></td><td>行最小高度(≥44px 触控最小边长)</td></tr>
|
||
<tr><td><code>--kole-m-table-cell-padding</code></td><td><code>var(--kole-space-12)</code></td><td>单元格左右内边距</td></tr>
|
||
<tr><td><code>--kole-m-table-min-width</code></td><td><code>0</code></td><td>内容最小宽度;超过容器即横向滚动</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="usage">何时使用</h2>
|
||
<ul>
|
||
<li>把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合</li>
|
||
<li>移动端表格必须在组件自己的容器里横向滚动,绝不能让页面整体横向溢出,否则整页会跟着左右晃</li>
|
||
<li>横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽</li>
|
||
<li>表头用 position: sticky 而不是 fixed —— fixed 会脱出滚动容器,横向滚动时表头不跟随</li>
|
||
<li>长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比</li>
|
||
<li>点整行即选中(同表互斥);行内的独立控件要阻止冒泡,避免一次点击触发两个动作</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="interaction">交互与触控</h2>
|
||
<ul>
|
||
<li>横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽、页面整体横向溢出</li>
|
||
<li>选项行高不小于 44px;触屏上横向滑动不被父级纵向滚动吞掉(touch-action: pan-x pan-y)</li>
|
||
<li>表头用 position: sticky 而不是 fixed——fixed 会脱出滚动容器,横向滚动时表头不跟随</li>
|
||
<li>长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比</li>
|
||
<li>点整行即选中(同表互斥);行内的独立控件(按钮 / 链接)要阻止冒泡,避免一次点击触发两个动作</li>
|
||
<li>行可点时同时提供键盘路径(聚焦后回车 / 空格),不把鼠标点击当成唯一入口</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="a11y">无障碍</h2>
|
||
<ul>
|
||
<li>用原生 table / thead / tbody / tr / th / td,表头写 scope="col"(读屏能报出列名)</li>
|
||
<li>空态用 role="status" + aria-live="polite" 播报占位文案</li>
|
||
<li>禁用行写 aria-disabled="true",读屏播报不可用</li>
|
||
<li>可点行必须同时可聚焦:clickable=true 时行写 tabindex="0" + role="row" + aria-selected,聚焦后回车 / 空格即选中 —— 只有鼠标点击而没有键盘路径是缺陷,不是简化</li>
|
||
<li>焦点环画在行内第一个单元格上(tr 上的 outline 在部分浏览器不渲染)</li>
|
||
<li>mode=card 时字段名由 data-label 生成并参与读屏(键值对在视觉与语义上都成立)</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="related">相似组件</h2>
|
||
<p class="m-sub">从「该用哪一个」的角度区分;PC 端的对应实现见 <a href="../../index.html">PC 文档站</a>。</p>
|
||
<table class="m-table">
|
||
<thead><tr><th>组件</th><th>何时用它而不是本组件</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><a href="mobile-list.html">列表</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">List</em></td><td>一行只需「标题 + 值 + 箭头」时用列表,不需要列对齐;列对比才是表格的强项</td></tr>
|
||
<tr><td><a href="cell.html">单元格</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">Cell</em></td><td>键值对数量很少(三四项)时用单元格纵向排列,比建一张两列表格更轻</td></tr>
|
||
<tr><td><a href="mobile-collapse.html">折叠面板</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">Collapse</em></td><td>需要展开某一行看明细时用折叠面板承载详情,表格本身不做行内展开</td></tr>
|
||
<tr><td><a href="mobile-loading.html">加载</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">Loading</em></td><td>表格数据加载中用它占位,不要在表体里自己画骨架屏</td></tr>
|
||
<tr><td><a href="mobile-tag.html">标签</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">Tag</em></td><td>状态列用小标签呈现(已发货 / 待付款)比纯文字更好扫读</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="gaps">规格未定 / 禁止发明</h2>
|
||
<table class="m-table">
|
||
<thead><tr><th>类别</th><th>条目</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="m-chip">禁止发明</span></td><td>虚拟滚动与无限加载的触发规则(由宿主容器负责;本组件不接管纵向滚动)</td></tr>
|
||
<tr><td><span class="m-chip">禁止发明</span></td><td>服务端排序 / 筛选 / 分页(排序图标与请求都由宿主发起)</td></tr>
|
||
<tr><td><span class="m-chip">禁止发明</span></td><td>列宽拖拽调整与列显隐设置(移动端没有这个操作面)</td></tr>
|
||
<tr><td><span class="m-chip">禁止发明</span></td><td>冻结列(position: sticky 的横向版本;当前只固定表头的纵向位置)</td></tr>
|
||
<tr><td><span class="m-chip">规格未定</span></td><td>表头是否需要在纵向滚动时也吸顶(当前只在宿主给固定高度时生效)</td></tr>
|
||
<tr><td><span class="m-chip">规格未定</span></td><td>行选中是单选还是多选(当前单选;多选应交给 Checkbox 列)</td></tr>
|
||
<tr><td><span class="m-chip">规格未定</span></td><td>卡片模式下字段名的排列是「左标签右值」还是「上标签下值」</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="anatomy">结构(anatomy)</h2>
|
||
<table class="m-table">
|
||
<thead><tr><th>字段</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>table</code></td><td>根元素,纵向排列「视口 + 空态」</td></tr>
|
||
<tr><td><code>table__viewport</code></td><td>横向滚动的唯一发生点(overflow-x: auto),页面整体不横向溢出</td></tr>
|
||
<tr><td><code>table__inner</code></td><td>原生 table,min-width 由 --kole-m-table-min-width 给出;超过容器即滚</td></tr>
|
||
<tr><td><code>table__caption</code></td><td>可选表格标题(说明这张表在讲什么)</td></tr>
|
||
<tr><td><code>table__head</code></td><td>表头,position: sticky 固定在滚动视口顶部</td></tr>
|
||
<tr><td><code>table__head-cell</code></td><td>表头单元格,写 scope=col 让读屏报出列名</td></tr>
|
||
<tr><td><code>table__row</code></td><td>数据行,行高不小于 44px</td></tr>
|
||
<tr><td><code>table__cell</code></td><td>单元格,可带 data-label(卡片模式的字段名来源)</td></tr>
|
||
<tr><td><code>table__empty</code></td><td>空态占位,role=status + aria-live=polite</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="variants">变体维度与类名映射</h2>
|
||
<p class="m-sub">类名映射由构建脚本从契约 <code>variantClasses</code> 生成,并被 <code>verify:mobile-docs</code> 逐条对照组件 CSS 校验(类/变量必须真实存在)。</p>
|
||
<table class="m-table">
|
||
<thead><tr><th>维度</th><th>取值</th><th>对应类名 / 变量</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>mode</code></td><td>scroll / card</td><td><div><b>scroll</b> <span class="m-cls">.kole-m-table--scroll</span></div><div><b>card</b> <span class="m-cls">.kole-m-table--card</span></div></td></tr>
|
||
<tr><td><code>size</code></td><td>default / compact</td><td><div><b>default</b> <span class="m-none">(由数据驱动,无专属类)</span></div><div><b>compact</b> <span class="m-cls">.kole-m-table--compact</span></div></td></tr>
|
||
<tr><td><code>stripe</code></td><td>false / true</td><td><div><b>false</b> <span class="m-none">(由数据驱动,无专属类)</span></div><div><b>true</b> <span class="m-cls">.kole-m-table--stripe</span></div></td></tr>
|
||
<tr><td><code>bordered</code></td><td>false / true</td><td><div><b>false</b> <span class="m-none">(由数据驱动,无专属类)</span></div><div><b>true</b> <span class="m-cls">.kole-m-table--bordered</span></div></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="rep-variants">代表变体</h2>
|
||
<table class="m-table">
|
||
<thead><tr><th>变体</th><th>标签</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>mode=scroll · size=default · stripe=false · bordered=true</code></td><td>横向滚动(列多时在组件容器里左右滑)</td></tr>
|
||
<tr><td><code>mode=scroll · size=default · stripe=true · bordered=false</code></td><td>斑马纹(偶数行浅底,便于对齐行)</td></tr>
|
||
<tr><td><code>mode=card · size=default · stripe=false · bordered=false</code></td><td>卡片模式(每行摊成键值对,小屏不横滚)</td></tr>
|
||
<tr><td><code>mode=scroll · size=compact · stripe=false · bordered=true</code></td><td>紧凑(收紧内边距与字号,行高仍 ≥44px)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="tokens">用到的令牌</h2>
|
||
<p class="m-sub">构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。</p>
|
||
<div class="m-chips">
|
||
<span class="m-chip m" title="正文 / 列表项">--kole-m-font-size-body</span>
|
||
<span class="m-chip m" title="次级说明">--kole-m-font-size-label</span>
|
||
<span class="m-chip m" title="页面左右留白">--kole-m-gutter</span>
|
||
<span class="m-chip m" title="可点区域最小边长">--kole-m-touch-target</span>
|
||
<span class="m-chip inherit" title="分割线/边框">--kole-color-border</span>
|
||
<span class="m-chip inherit" title="品牌主色:主按钮、选中态、链接">--kole-color-brand</span>
|
||
<span class="m-chip inherit" title="品牌浅色背景:选中背景、标签背景、表格悬停">--kole-color-brand-bg</span>
|
||
<span class="m-chip inherit" title="卡片背景">--kole-color-card-bg</span>
|
||
<span class="m-chip inherit" title="输入框聚焦外发光">--kole-color-focus-ring</span>
|
||
<span class="m-chip inherit" title="表头背景">--kole-color-table-header-bg</span>
|
||
<span class="m-chip inherit" title="正文文字 · 14.0:1">--kole-color-text-body</span>
|
||
<span class="m-chip inherit" title="禁用态文字 · WCAG 1.4.3 对禁用控件豁免(规范定义于组件1/2.txt)">--kole-color-text-disabled</span>
|
||
<span class="m-chip inherit" title="次要文字 · 5.10:1(原 #8C8C8C 仅 3.36:1)">--kole-color-text-secondary</span>
|
||
<span class="m-chip inherit" title="">--kole-font-family</span>
|
||
<span class="m-chip inherit" title="">--kole-font-family-num</span>
|
||
<span class="m-chip inherit" title="按钮、输入框、选择器">--kole-radius-base</span>
|
||
<span class="m-chip inherit" title="">--kole-space-12</span>
|
||
<span class="m-chip inherit" title="">--kole-space-4</span>
|
||
<span class="m-chip inherit" title="">--kole-space-40</span>
|
||
<span class="m-chip" title="组件级变量(可在业务侧覆盖)">--kole-m-table-cell-padding</span>
|
||
<span class="m-chip" title="组件级变量(可在业务侧覆盖)">--kole-m-table-min-width</span>
|
||
<span class="m-chip" title="组件级变量(可在业务侧覆盖)">--kole-m-table-row-height</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="sources">6 端源码</h2>
|
||
<p class="m-sub">同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。</p>
|
||
<details class="m-details">
|
||
<summary>frameworks-mobile/Table.css · 纯样式(CSS) · 229 行</summary>
|
||
<div class="m-code">
|
||
<div class="m-code-head"><span>frameworks-mobile/Table.css</span><button class="m-copy" type="button" data-copy="code-265">复制</button></div>
|
||
<pre id="code-265"><code>/* Kole UI Mobile · Table 样式 — 对齐移动端规格 §47
|
||
表格:列多时必须能横向滚动,但**不得让页面整体横向溢出** ——
|
||
滚动发生在组件自己的容器上(overflow-x: auto),而不是文档上。
|
||
表头固定用 position: sticky(不用 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
|
||
行高不小于 44px;空态与加载态都在同一个容器内,不改变表格宽度。 */
|
||
|
||
.kole-m-table {
|
||
/* 组件级变量:业务侧可在容器上覆盖 */
|
||
--kole-m-table-row-height: var(--kole-m-touch-target); /* 行最小高度(≥44px 触控最小边长) */
|
||
--kole-m-table-cell-padding: var(--kole-space-12); /* 单元格左右内边距 */
|
||
--kole-m-table-min-width: 0; /* 内容最小宽度;超过容器即横向滚动 */
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
/* 横向滚动容器:min-width: 0 让它能在 flex 父级里收缩,不把父级顶宽 */
|
||
min-width: 0;
|
||
background: var(--kole-color-card-bg);
|
||
color: var(--kole-color-text-body);
|
||
font-family: var(--kole-font-family);
|
||
font-size: var(--kole-m-font-size-body);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* 滚动视口:横向滚动的唯一发生点。页面级横向溢出由它兜住。 */
|
||
.kole-m-table__viewport {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
overflow-x: auto;
|
||
overflow-y: visible;
|
||
-webkit-overflow-scrolling: touch;
|
||
/* 触屏上横向滑动不被父级纵向滚动吞掉 */
|
||
touch-action: pan-x pan-y;
|
||
}
|
||
|
||
.kole-m-table__caption {
|
||
margin: 0;
|
||
padding: var(--kole-m-table-cell-padding) var(--kole-m-gutter);
|
||
color: var(--kole-color-text-secondary);
|
||
font-size: var(--kole-m-font-size-label);
|
||
}
|
||
|
||
.kole-m-table__inner {
|
||
min-width: var(--kole-m-table-min-width);
|
||
margin: 0;
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 变体 mode=scroll:默认形态 —— 列多时在组件自己的视口里横向滚动 */
|
||
.kole-m-table--scroll .kole-m-table__viewport { overflow-x: auto; }
|
||
|
||
/* 变体 size=compact:行高 44px 不变(触控下限),只收紧上下内边距与字号 */
|
||
.kole-m-table--compact .kole-m-table__inner { font-size: var(--kole-m-font-size-label); }
|
||
|
||
/* 表头:sticky 在滚动视口顶部。由于视口只横向滚动、不设最大高度,
|
||
sticky 主要保证纵向滚动由页面承担时表头仍跟随(宿主给固定高度时也生效)。 */
|
||
.kole-m-table__head-cell {
|
||
box-sizing: border-box;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 2;
|
||
padding: var(--kole-m-table-cell-padding) var(--kole-m-table-cell-padding);
|
||
margin: 0;
|
||
background: var(--kole-color-table-header-bg);
|
||
color: var(--kole-color-text-secondary);
|
||
font-weight: 500;
|
||
font-size: var(--kole-m-font-size-label);
|
||
line-height: 1.3;
|
||
white-space: nowrap;
|
||
text-align: start;
|
||
border-bottom: 1px solid var(--kole-color-border);
|
||
}
|
||
|
||
/* 变体 bordered=true:单元格之间也画竖线 */
|
||
.kole-m-table--bordered .kole-m-table__head-cell + .kole-m-table__head-cell,
|
||
.kole-m-table--bordered .kole-m-table__cell + .kole-m-table__cell {
|
||
border-inline-start: 1px solid var(--kole-color-border);
|
||
}
|
||
|
||
/* 变体 stripe=true:偶数行浅底,便于横向扫读时对齐行 */
|
||
.kole-m-table--stripe .kole-m-table__row:nth-child(even) .kole-m-table__cell {
|
||
background: var(--kole-color-table-header-bg);
|
||
}
|
||
|
||
.kole-m-table__cell {
|
||
box-sizing: border-box;
|
||
padding: var(--kole-m-table-cell-padding);
|
||
margin: 0;
|
||
background: var(--kole-color-card-bg);
|
||
color: var(--kole-color-text-body);
|
||
font-size: inherit;
|
||
line-height: 1.4;
|
||
vertical-align: middle;
|
||
text-align: start;
|
||
}
|
||
|
||
.kole-m-table__row { min-height: var(--kole-m-table-row-height); }
|
||
|
||
.kole-m-table__row .kole-m-table__cell { border-bottom: 1px solid var(--kole-color-border); }
|
||
|
||
.kole-m-table__row:last-child .kole-m-table__cell { border-bottom: 0; }
|
||
|
||
/* 列对齐(变体 align 由列定义给出;这里是三档工具类) */
|
||
.kole-m-table__cell--start,
|
||
.kole-m-table__head-cell--start { text-align: start; }
|
||
|
||
.kole-m-table__cell--center,
|
||
.kole-m-table__head-cell--center { text-align: center; }
|
||
|
||
.kole-m-table__cell--end,
|
||
.kole-m-table__head-cell--end { text-align: end; }
|
||
|
||
/* 数字列:等宽字 + 右对齐,方便按位对比 */
|
||
.kole-m-table__cell--number,
|
||
.kole-m-table__head-cell--number {
|
||
font-family: var(--kole-font-family-num);
|
||
text-align: end;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* 省略:长文本列默认单行省略,完整值由 title 承载 */
|
||
.kole-m-table__cell--ellipsis {
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
max-width: 160px;
|
||
}
|
||
|
||
/* 状态 clickable:整行是热区(clickable=true)。
|
||
可点行必须**同时可聚焦**(tabindex="0" + role="row")—— 否则点击成了唯一的交互路径,
|
||
键盘与读屏用户点不到(规格 §47.6 明说「整行点击不能是唯一的键盘路径」)。
|
||
cursor 只给表体行:表头行不承载点击,跟着变手型会把「这里能点」这个信号弄错。 */
|
||
.kole-m-table__body .kole-m-table__row { cursor: pointer; }
|
||
|
||
.kole-m-table__body .kole-m-table__row:active .kole-m-table__cell {
|
||
background: var(--kole-color-brand-bg);
|
||
}
|
||
|
||
/* 可聚焦行的焦点环:画在行内第一个单元格上(tr 上画 outline 在部分浏览器不渲染) */
|
||
.kole-m-table__row:focus-visible .kole-m-table__cell:first-child {
|
||
outline: 2px solid var(--kole-color-focus-ring);
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
/* 状态 selected:当前选中行 */
|
||
.kole-m-table__row.is-selected .kole-m-table__cell {
|
||
background: var(--kole-color-brand-bg);
|
||
color: var(--kole-color-brand);
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 状态 disabled:置灰且不响应 */
|
||
.kole-m-table__row.is-disabled .kole-m-table__cell {
|
||
color: var(--kole-color-text-disabled);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 行内可聚焦元素(链接 / 按钮)的焦点环 */
|
||
.kole-m-table__cell a:focus-visible,
|
||
.kole-m-table__cell button:focus-visible {
|
||
outline: 2px solid var(--kole-color-focus-ring);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* 状态 empty:空态占位(不改变表格列宽语义,直接替换表格区) */
|
||
.kole-m-table__empty {
|
||
margin: 0;
|
||
padding: var(--kole-space-40) var(--kole-m-gutter);
|
||
background: var(--kole-color-card-bg);
|
||
color: var(--kole-color-text-secondary);
|
||
font-size: var(--kole-m-font-size-label);
|
||
text-align: center;
|
||
border-bottom: 1px solid var(--kole-color-border);
|
||
}
|
||
|
||
/* 变体 mode=card:小屏上把每行渲染成一张卡(键值对纵排),彻底不横向滚动 */
|
||
.kole-m-table--card .kole-m-table__viewport { overflow-x: visible; }
|
||
|
||
.kole-m-table--card .kole-m-table__inner { display: block; }
|
||
|
||
.kole-m-table--card .kole-m-table__head { display: none; }
|
||
|
||
.kole-m-table--card .kole-m-table__row {
|
||
display: block;
|
||
padding: var(--kole-m-table-cell-padding) var(--kole-m-gutter);
|
||
border-bottom: 1px solid var(--kole-color-border);
|
||
}
|
||
|
||
.kole-m-table--card .kole-m-table__row .kole-m-table__cell {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: var(--kole-space-12);
|
||
padding: var(--kole-space-4) 0;
|
||
border-bottom: 0;
|
||
}
|
||
|
||
/* 卡片模式下的字段名:由列的 label 生成(data-label 由实现写出) */
|
||
.kole-m-table--card .kole-m-table__cell::before {
|
||
content: attr(data-label);
|
||
flex: 0 0 auto;
|
||
color: var(--kole-color-text-secondary);
|
||
font-size: var(--kole-m-font-size-label);
|
||
}
|
||
|
||
/* 演示页触发器(非组件交付样式) */
|
||
.kole-m-table__demo-btn {
|
||
box-sizing: border-box;
|
||
min-height: var(--kole-m-touch-target);
|
||
padding: 0 var(--kole-m-gutter);
|
||
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-label);
|
||
cursor: pointer;
|
||
touch-action: manipulation;
|
||
}
|
||
|
||
.kole-m-table__demo-btn:focus-visible {
|
||
outline: 2px solid var(--kole-color-focus-ring);
|
||
outline-offset: 2px;
|
||
}
|
||
</code></pre>
|
||
</div>
|
||
</details>
|
||
<details class="m-details">
|
||
<summary>frameworks-mobile/Table.html · H5 原生(无框架) · 256 行</summary>
|
||
<div class="m-code">
|
||
<div class="m-code-head"><span>frameworks-mobile/Table.html</span><button class="m-copy" type="button" data-copy="code-266">复制</button></div>
|
||
<pre id="code-266"><code><!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 · Table(H5)</title>
|
||
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
|
||
<link rel="stylesheet" href="Table.css">
|
||
<style>
|
||
body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
|
||
/* 关键:.demo 与各演示块都写 min-width: 0,否则内部表格的 min-width 会把页面顶宽 */
|
||
.demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 0; }
|
||
.demo-block { min-width: 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-pad { min-width: 0; padding: 0 0 var(--kole-space-8); background: var(--kole-color-card-bg);
|
||
border-block: 1px solid var(--kole-color-border); }
|
||
/* 横向滚动的提示条:说明「这里可以左右滑」,不是组件的一部分 */
|
||
.demo-hint { margin: 0; padding: var(--kole-space-8) var(--kole-m-gutter) 0;
|
||
font-size: var(--kole-m-font-size-caption); color: var(--kole-color-text-secondary); }
|
||
.demo-block[hidden] { display: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="demo">
|
||
<section class="demo-block" data-demo="scroll">
|
||
<p class="demo-label">横向滚动(列多时在表格自己的容器里左右滑;页面整体不横向溢出)</p>
|
||
<div class="demo-pad" data-assert="table-scroll">
|
||
<p class="demo-hint">← 左右滑动查看全部 6 列 →</p>
|
||
<div class="kole-m-table kole-m-table--bordered" id="tb-scroll"
|
||
style="--kole-m-table-min-width: 560px" data-assert="table-scroll-wide">
|
||
<div class="kole-m-table__viewport" id="tb-scroll-viewport">
|
||
<table class="kole-m-table__inner">
|
||
<caption class="kole-m-table__caption">近 3 日订单</caption>
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">客户</th>
|
||
<th class="kole-m-table__head-cell" scope="col">状态</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
|
||
<th class="kole-m-table__head-cell" scope="col">下单时间</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--end" scope="col">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-001</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">王小明</td>
|
||
<td class="kole-m-table__cell">待发货</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 1,280.00</td>
|
||
<td class="kole-m-table__cell">09-20 09:12</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-002</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">李小红</td>
|
||
<td class="kole-m-table__cell">已发货</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 368.50</td>
|
||
<td class="kole-m-table__cell">09-20 10:35</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-003</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">赵大力</td>
|
||
<td class="kole-m-table__cell">已完成</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 5,600.00</td>
|
||
<td class="kole-m-table__cell">09-20 14:08</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="demo-block" data-demo="clickable">
|
||
<p class="demo-label">表头固定 + 列对齐 + 行点击(点整行选中该行;数字列等宽右对齐,便于按位对比)</p>
|
||
<div class="demo-pad" data-assert="table-clickable">
|
||
<div class="kole-m-table kole-m-table--clickable kole-m-table--stripe kole-m-table--bordered" id="tb-clickable"
|
||
data-selected="" data-assert="table-clickable-grid">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">商品</th>
|
||
<th class="kole-m-table__head-cell" scope="col">仓库</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">库存</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">单价</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row" data-row="sku-1" tabindex="0" role="row" aria-selected="false"
|
||
data-behavior="click-sets-attr:#tb-clickable|data-selected|sku-1">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">有机棉 T 恤</td>
|
||
<td class="kole-m-table__cell">杭州仓</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">1,240</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 89.00</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row" data-row="sku-2" tabindex="0" role="row" aria-selected="false">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">轻薄羽绒服</td>
|
||
<td class="kole-m-table__cell">广州仓</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">86</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 459.00</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row is-selected" data-row="sku-3" tabindex="0" role="row" aria-selected="true">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">帆布托特包</td>
|
||
<td class="kole-m-table__cell">杭州仓</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">512</td>
|
||
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 129.00</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="demo-block" data-demo="card">
|
||
<p class="demo-label">卡片模式(mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动)</p>
|
||
<div class="demo-pad" data-assert="table-card">
|
||
<div class="kole-m-table kole-m-table--card" id="tb-card">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell" scope="col">客户</th>
|
||
<th class="kole-m-table__head-cell" scope="col">等级</th>
|
||
<th class="kole-m-table__head-cell" scope="col">本月消费</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell" data-label="客户">王小明</td>
|
||
<td class="kole-m-table__cell" data-label="等级">黄金</td>
|
||
<td class="kole-m-table__cell" data-label="本月消费">¥ 1,280.00</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell" data-label="客户">李小红</td>
|
||
<td class="kole-m-table__cell" data-label="等级">白银</td>
|
||
<td class="kole-m-table__cell" data-label="本月消费">¥ 368.50</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="demo-block" data-demo="empty">
|
||
<p class="demo-label">空态(没有数据时显示占位文案;列宽语义不变,不会把容器撑宽)</p>
|
||
<div class="demo-pad" data-assert="table-empty">
|
||
<div class="kole-m-table" id="tb-empty" data-empty="true">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
|
||
<th class="kole-m-table__head-cell" scope="col">状态</th>
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
<p class="kole-m-table__empty" role="status" aria-live="polite">当前筛选条件下没有订单</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="demo-block" data-demo="states">
|
||
<p class="demo-label">禁用行与省略列(禁用行置灰不响应;长文本列单行省略,完整值在 title 里)</p>
|
||
<div class="demo-pad" data-assert="table-states">
|
||
<div class="kole-m-table kole-m-table--bordered" id="tb-states">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">收货地址</th>
|
||
<th class="kole-m-table__head-cell" scope="col">配送</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="kole-m-table__row">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start kole-m-table__cell--ellipsis"
|
||
title="浙江省杭州市西湖区文三路 100 号 1 幢 1801 室">浙江省杭州市西湖区文三路 100 号 1 幢 1801 室</td>
|
||
<td class="kole-m-table__cell">顺丰</td>
|
||
</tr>
|
||
<tr class="kole-m-table__row is-disabled" aria-disabled="true">
|
||
<td class="kole-m-table__cell kole-m-table__cell--start">广东省深圳市南山区科技园南区</td>
|
||
<td class="kole-m-table__cell">暂不可达</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<script>
|
||
/* 演示页交互:点整行即选中(同表互斥),选中态写 is-selected 与 data-selected。
|
||
横向滚动由 .kole-m-table__viewport 自己承担 —— 页面整体不横向溢出。
|
||
每块各自独立:状态只落在本块 DOM 上。 */
|
||
(function () {
|
||
Array.prototype.forEach.call(document.querySelectorAll('.kole-m-table--clickable'), function (root) {
|
||
root.addEventListener('click', function (e) {
|
||
var row = e.target.closest ? e.target.closest('.kole-m-table__row') : null;
|
||
if (!row || !root.contains(row) || row.classList.contains('is-disabled')) return;
|
||
if (row.getAttribute('aria-disabled') === 'true') return;
|
||
Array.prototype.forEach.call(root.querySelectorAll('.kole-m-table__row'), function (r) {
|
||
var on = r === row;
|
||
r.classList.toggle('is-selected', on);
|
||
r.setAttribute('aria-selected', on ? 'true' : 'false');
|
||
});
|
||
root.setAttribute('data-selected', row.getAttribute('data-row') || '');
|
||
});
|
||
|
||
/* 键盘路径:聚焦后按回车 / 空格选中该行(整行点击不能是唯一的交互路径) */
|
||
root.addEventListener('keydown', function (e) {
|
||
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
|
||
var row = e.target.closest ? e.target.closest('.kole-m-table__row') : null;
|
||
if (!row || !root.contains(row) || row.classList.contains('is-disabled')) return;
|
||
e.preventDefault();
|
||
Array.prototype.forEach.call(root.querySelectorAll('.kole-m-table__row'), function (r) {
|
||
var on = r === row;
|
||
r.classList.toggle('is-selected', on);
|
||
r.setAttribute('aria-selected', on ? 'true' : 'false');
|
||
});
|
||
root.setAttribute('data-selected', row.getAttribute('data-row') || '');
|
||
});
|
||
});
|
||
})();
|
||
</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>
|
||
</code></pre>
|
||
</div>
|
||
</details>
|
||
<details class="m-details">
|
||
<summary>frameworks-mobile/Table.jsx · React · 152 行</summary>
|
||
<div class="m-code">
|
||
<div class="m-code-head"><span>frameworks-mobile/Table.jsx</span><button class="m-copy" type="button" data-copy="code-267">复制</button></div>
|
||
<pre id="code-267"><code>import React from 'react';
|
||
import './Table.css';
|
||
|
||
/* 表格(移动端)— 规格 §47。
|
||
结构:table(根)> viewport(横向滚动视口)> inner(原生 table)+ empty(空态)。
|
||
**横向滚动只发生在 viewport 上**(overflow-x: auto),页面整体不横向溢出(规格 §47.1);
|
||
根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽。
|
||
表头 sticky(不是 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
|
||
mode=card 把小屏行摊成键值对,彻底不需要横向滚动。
|
||
rows 为二维数组或对象数组;cells 支持 { text, align, ellipsis, number, title }。 */
|
||
|
||
const ALIGN = { start: 'start', center: 'center', end: 'end', number: 'number' };
|
||
|
||
function cellOf(row, col, ci) {
|
||
if (Array.isArray(row)) return { text: row[ci] };
|
||
if (row && typeof row === 'object') {
|
||
const raw = row[col.key === undefined ? ci : col.key];
|
||
if (raw && typeof raw === 'object') return raw;
|
||
return { text: raw };
|
||
}
|
||
return { text: '' };
|
||
}
|
||
|
||
function textOf(cell) {
|
||
if (cell === null || cell === undefined) return '';
|
||
return String(cell.text === undefined ? cell : cell.text);
|
||
}
|
||
|
||
function alignOf(col, cell) {
|
||
if (cell && cell.number) return 'number';
|
||
if (cell && cell.align && ALIGN[cell.align]) return cell.align;
|
||
if (col && col.number) return 'number';
|
||
if (col && col.align && ALIGN[col.align]) return col.align;
|
||
return 'start';
|
||
}
|
||
|
||
export default function Table({
|
||
columns = [],
|
||
rows = [],
|
||
mode = 'scroll',
|
||
size = 'default',
|
||
stripe = false,
|
||
bordered = false,
|
||
clickable = false,
|
||
caption = '',
|
||
emptyText = '暂无数据',
|
||
onSelect,
|
||
children = null,
|
||
}) {
|
||
const card = mode === 'card';
|
||
const cls =
|
||
'kole-m-table' +
|
||
` kole-m-table--${card ? 'card' : 'scroll'}` +
|
||
(size === 'compact' ? ' kole-m-table--compact' : '') +
|
||
(stripe ? ' kole-m-table--stripe' : '') +
|
||
(bordered ? ' kole-m-table--bordered' : '') +
|
||
(clickable ? ' kole-m-table--clickable' : '');
|
||
|
||
const cols = (Array.isArray(columns) ? columns : []).map((c, i) =>
|
||
c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i }
|
||
);
|
||
|
||
return (
|
||
<div className={cls} data-mode={card ? 'card' : 'scroll'}>
|
||
<div className="kole-m-table__viewport">
|
||
<table className="kole-m-table__inner">
|
||
{caption ? <caption className="kole-m-table__caption">{caption}</caption> : null}
|
||
<thead className="kole-m-table__head">
|
||
<tr className="kole-m-table__row">
|
||
{cols.map((col, ci) => (
|
||
<th
|
||
className={
|
||
'kole-m-table__head-cell' +
|
||
(col && col.number ? ' kole-m-table__head-cell--number' : '') +
|
||
(col && col.align ? ' kole-m-table__head-cell--' + col.align : '')
|
||
}
|
||
scope="col"
|
||
key={col.key === undefined ? ci : String(col.key)}
|
||
>
|
||
{col && col.title !== undefined ? col.title : ''}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
{rows.length ? (
|
||
<tbody className="kole-m-table__body">
|
||
{rows.map((row, ri) => {
|
||
const rowDisabled = !!(row && row.disabled);
|
||
const rowSelected = !!(row && row.selected);
|
||
/* 可点行必须同时可聚焦:整行点击不能是唯一的键盘路径(规格 §47.6) */
|
||
const rowProps = clickable && !rowDisabled
|
||
? {
|
||
tabIndex: 0,
|
||
role: 'row',
|
||
'aria-selected': rowSelected ? 'true' : 'false',
|
||
onKeyDown: (e) => {
|
||
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
|
||
e.preventDefault();
|
||
if (onSelect) onSelect(ri, row);
|
||
},
|
||
}
|
||
: { role: 'row' };
|
||
return (
|
||
<tr
|
||
className={
|
||
'kole-m-table__row' +
|
||
(rowSelected ? ' is-selected' : '') +
|
||
(rowDisabled ? ' is-disabled' : '')
|
||
}
|
||
key={row && row.key !== undefined ? String(row.key) : ri}
|
||
aria-disabled={rowDisabled ? 'true' : undefined}
|
||
{...rowProps}
|
||
onClick={() => {
|
||
if (rowDisabled) return;
|
||
if (onSelect) onSelect(ri, row);
|
||
}}
|
||
>
|
||
{cols.map((col, ci) => {
|
||
const cell = cellOf(row, col, ci);
|
||
const align = alignOf(col, cell);
|
||
return (
|
||
<td
|
||
className={
|
||
'kole-m-table__cell' +
|
||
` kole-m-table__cell--${align}` +
|
||
(cell && cell.ellipsis ? ' kole-m-table__cell--ellipsis' : '')
|
||
}
|
||
key={(col.key === undefined ? ci : String(col.key)) + '-' + ri}
|
||
data-label={col && col.title !== undefined ? String(col.title) : ''}
|
||
title={cell && cell.title ? String(cell.title) : undefined}
|
||
>
|
||
{textOf(cell)}
|
||
</td>
|
||
);
|
||
})}
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
) : null}
|
||
</table>
|
||
</div>
|
||
{rows.length ? null : (
|
||
<p className="kole-m-table__empty" role="status" aria-live="polite">
|
||
{emptyText}
|
||
</p>
|
||
)}
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
</code></pre>
|
||
</div>
|
||
</details>
|
||
<details class="m-details">
|
||
<summary>frameworks-mobile/Table.vue2.vue · Vue 2 · 144 行</summary>
|
||
<div class="m-code">
|
||
<div class="m-code-head"><span>frameworks-mobile/Table.vue2.vue</span><button class="m-copy" type="button" data-copy="code-268">复制</button></div>
|
||
<pre id="code-268"><code><template>
|
||
<div class="kole-m-table" :class="rootClass" :data-mode="isCard ? 'card' : 'scroll'">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<caption v-if="caption" class="kole-m-table__caption">{{ caption }}</caption>
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th
|
||
v-for="(col, ci) in cols"
|
||
:key="keyOf(col, ci)"
|
||
class="kole-m-table__head-cell"
|
||
:class="headClass(col)"
|
||
scope="col"
|
||
>{{ col && col.title !== undefined ? col.title : '' }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody v-if="rows.length" class="kole-m-table__body">
|
||
<tr
|
||
v-for="(row, ri) in rows"
|
||
:key="row && row.key !== undefined ? String(row.key) : ri"
|
||
class="kole-m-table__row"
|
||
:class="{ 'is-selected': !!(row && row.selected), 'is-disabled': !!(row && row.disabled) }"
|
||
role="row"
|
||
:aria-selected="clickable && !(row && row.disabled) ? (row && row.selected ? 'true' : 'false') : null"
|
||
:aria-disabled="row && row.disabled ? 'true' : null"
|
||
:tabindex="clickable && !(row && row.disabled) ? 0 : null"
|
||
@click="onRowClick(ri, row)"
|
||
@keydown="onRowKey(ri, row, $event)"
|
||
>
|
||
<td
|
||
v-for="(col, ci) in cols"
|
||
:key="keyOf(col, ci) + '-' + ri"
|
||
class="kole-m-table__cell"
|
||
:class="cellClass(col, row, ci)"
|
||
:data-label="col && col.title !== undefined ? String(col.title) : ''"
|
||
:title="titleOf(row, col, ci)"
|
||
>{{ textOf(cellOf(row, col, ci)) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p v-if="!rows.length" class="kole-m-table__empty" role="status" aria-live="polite">{{ emptyText }}</p>
|
||
<slot></slot>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
/* 表格(移动端)— 规格 §47。
|
||
结构:table(根)> viewport(横向滚动视口)> inner(原生 table)+ empty(空态)。
|
||
**横向滚动只发生在 viewport 上**(overflow-x: auto),页面整体不横向溢出(规格 §47.1);
|
||
根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽。
|
||
表头 sticky(不是 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
|
||
mode=card 把小屏行摊成键值对,彻底不需要横向滚动。
|
||
rows 为二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }。 */
|
||
|
||
export default {
|
||
name: 'KoleMTable',
|
||
props: {
|
||
columns: { type: Array, default: function () { return []; } },
|
||
rows: { type: Array, default: function () { return []; } },
|
||
mode: { type: String, default: 'scroll' },
|
||
size: { type: String, default: 'default' },
|
||
stripe: { type: Boolean, default: false },
|
||
bordered: { type: Boolean, default: false },
|
||
clickable: { type: Boolean, default: false },
|
||
caption: { type: String, default: '' },
|
||
emptyText: { type: String, default: '暂无数据' }
|
||
},
|
||
computed: {
|
||
isCard: function () {
|
||
return this.mode === 'card';
|
||
},
|
||
cols: function () {
|
||
return (Array.isArray(this.columns) ? this.columns : []).map(function (c, i) {
|
||
return c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i };
|
||
});
|
||
},
|
||
rootClass: function () {
|
||
return [
|
||
'kole-m-table--' + (this.isCard ? 'card' : 'scroll'),
|
||
this.size === 'compact' ? 'kole-m-table--compact' : '',
|
||
this.stripe ? 'kole-m-table--stripe' : '',
|
||
this.bordered ? 'kole-m-table--bordered' : '',
|
||
this.clickable ? 'kole-m-table--clickable' : ''
|
||
].filter(Boolean);
|
||
}
|
||
},
|
||
methods: {
|
||
keyOf: function (col, ci) {
|
||
return col && col.key !== undefined ? String(col.key) : String(ci);
|
||
},
|
||
cellOf: function (row, col, ci) {
|
||
if (Array.isArray(row)) return { text: row[ci] };
|
||
if (row && typeof row === 'object') {
|
||
var raw = row[col && col.key !== undefined ? col.key : ci];
|
||
if (raw && typeof raw === 'object') return raw;
|
||
return { text: raw };
|
||
}
|
||
return { text: '' };
|
||
},
|
||
textOf: function (cell) {
|
||
if (cell === null || cell === undefined) return '';
|
||
return String(cell.text === undefined ? cell : cell.text);
|
||
},
|
||
titleOf: function (row, col, ci) {
|
||
var cell = this.cellOf(row, col, ci);
|
||
return cell && cell.title ? String(cell.title) : null;
|
||
},
|
||
alignOf: function (col, cell) {
|
||
if (cell && cell.number) return 'number';
|
||
if (cell && cell.align) return cell.align;
|
||
if (col && col.number) return 'number';
|
||
if (col && col.align) return col.align;
|
||
return 'start';
|
||
},
|
||
headClass: function (col) {
|
||
if (col && col.number) return 'kole-m-table__head-cell--number';
|
||
if (col && col.align) return 'kole-m-table__head-cell--' + col.align;
|
||
return '';
|
||
},
|
||
cellClass: function (col, row, ci) {
|
||
var cell = this.cellOf(row, col, ci);
|
||
return [
|
||
'kole-m-table__cell--' + this.alignOf(col, cell),
|
||
cell && cell.ellipsis ? 'kole-m-table__cell--ellipsis' : ''
|
||
].filter(Boolean);
|
||
},
|
||
onRowClick: function (ri, row) {
|
||
if (row && row.disabled) return;
|
||
this.$emit('select', ri, row);
|
||
},
|
||
/* 键盘路径:聚焦后按回车 / 空格选中该行(整行点击不能是唯一的交互路径,规格 §47.6) */
|
||
onRowKey: function (ri, row, e) {
|
||
if (!this.clickable || (row && row.disabled)) return;
|
||
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
|
||
e.preventDefault();
|
||
this.$emit('select', ri, row);
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style src="./Table.css"></style>
|
||
</code></pre>
|
||
</div>
|
||
</details>
|
||
<details class="m-details">
|
||
<summary>frameworks-mobile/Table.vue3.vue · Vue 3 · 142 行</summary>
|
||
<div class="m-code">
|
||
<div class="m-code-head"><span>frameworks-mobile/Table.vue3.vue</span><button class="m-copy" type="button" data-copy="code-269">复制</button></div>
|
||
<pre id="code-269"><code><template>
|
||
<div class="kole-m-table" :class="rootClass" :data-mode="isCard ? 'card' : 'scroll'">
|
||
<div class="kole-m-table__viewport">
|
||
<table class="kole-m-table__inner">
|
||
<caption v-if="caption" class="kole-m-table__caption">{{ caption }}</caption>
|
||
<thead class="kole-m-table__head">
|
||
<tr class="kole-m-table__row">
|
||
<th
|
||
v-for="(col, ci) in cols"
|
||
:key="keyOf(col, ci)"
|
||
class="kole-m-table__head-cell"
|
||
:class="headClass(col)"
|
||
scope="col"
|
||
>{{ col && col.title !== undefined ? col.title : '' }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody v-if="rows.length" class="kole-m-table__body">
|
||
<tr
|
||
v-for="(row, ri) in rows"
|
||
:key="row && row.key !== undefined ? String(row.key) : ri"
|
||
class="kole-m-table__row"
|
||
:class="{ 'is-selected': !!(row && row.selected), 'is-disabled': !!(row && row.disabled) }"
|
||
role="row"
|
||
:aria-selected="clickable && !(row && row.disabled) ? (row && row.selected ? 'true' : 'false') : null"
|
||
:aria-disabled="row && row.disabled ? 'true' : null"
|
||
:tabindex="clickable && !(row && row.disabled) ? 0 : null"
|
||
@click="onRowClick(ri, row)"
|
||
@keydown="onRowKey(ri, row, $event)"
|
||
>
|
||
<td
|
||
v-for="(col, ci) in cols"
|
||
:key="keyOf(col, ci) + '-' + ri"
|
||
class="kole-m-table__cell"
|
||
:class="cellClass(col, row, ci)"
|
||
:data-label="col && col.title !== undefined ? String(col.title) : ''"
|
||
:title="cellOf(row, col, ci).title ? String(cellOf(row, col, ci).title) : null"
|
||
>{{ textOf(cellOf(row, col, ci)) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p v-if="!rows.length" class="kole-m-table__empty" role="status" aria-live="polite">{{ emptyText }}</p>
|
||
<slot></slot>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
/* 表格(移动端)— 规格 §47。
|
||
结构:table(根)> viewport(横向滚动视口)> inner(原生 table)+ empty(空态)。
|
||
**横向滚动只发生在 viewport 上**(overflow-x: auto),页面整体不横向溢出(规格 §47.1);
|
||
根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽。
|
||
表头 sticky(不是 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
|
||
mode=card 把小屏行摊成键值对,彻底不需要横向滚动。
|
||
rows 为二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }。 */
|
||
import { computed } from 'vue';
|
||
|
||
const props = defineProps({
|
||
columns: { type: Array, default: () => [] },
|
||
rows: { type: Array, default: () => [] },
|
||
mode: { type: String, default: 'scroll' },
|
||
size: { type: String, default: 'default' },
|
||
stripe: { type: Boolean, default: false },
|
||
bordered: { type: Boolean, default: false },
|
||
clickable: { type: Boolean, default: false },
|
||
caption: { type: String, default: '' },
|
||
emptyText: { type: String, default: '暂无数据' }
|
||
});
|
||
const emit = defineEmits(['select']);
|
||
|
||
const isCard = computed(() => props.mode === 'card');
|
||
|
||
const cols = computed(() =>
|
||
(Array.isArray(props.columns) ? props.columns : []).map((c, i) =>
|
||
c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i }
|
||
)
|
||
);
|
||
|
||
const rootClass = computed(() => [
|
||
'kole-m-table--' + (isCard.value ? 'card' : 'scroll'),
|
||
props.size === 'compact' ? 'kole-m-table--compact' : '',
|
||
props.stripe ? 'kole-m-table--stripe' : '',
|
||
props.bordered ? 'kole-m-table--bordered' : '',
|
||
props.clickable ? 'kole-m-table--clickable' : ''
|
||
].filter(Boolean));
|
||
|
||
function keyOf(col, ci) {
|
||
return col && col.key !== undefined ? String(col.key) : String(ci);
|
||
}
|
||
|
||
function cellOf(row, col, ci) {
|
||
if (Array.isArray(row)) return { text: row[ci] };
|
||
if (row && typeof row === 'object') {
|
||
const raw = row[col && col.key !== undefined ? col.key : ci];
|
||
if (raw && typeof raw === 'object') return raw;
|
||
return { text: raw };
|
||
}
|
||
return { text: '' };
|
||
}
|
||
|
||
function textOf(cell) {
|
||
if (cell === null || cell === undefined) return '';
|
||
return String(cell.text === undefined ? cell : cell.text);
|
||
}
|
||
|
||
function alignOf(col, cell) {
|
||
if (cell && cell.number) return 'number';
|
||
if (cell && cell.align) return cell.align;
|
||
if (col && col.number) return 'number';
|
||
if (col && col.align) return col.align;
|
||
return 'start';
|
||
}
|
||
|
||
function headClass(col) {
|
||
if (col && col.number) return 'kole-m-table__head-cell--number';
|
||
if (col && col.align) return 'kole-m-table__head-cell--' + col.align;
|
||
return '';
|
||
}
|
||
|
||
function cellClass(col, row, ci) {
|
||
const cell = cellOf(row, col, ci);
|
||
return [
|
||
'kole-m-table__cell--' + alignOf(col, cell),
|
||
cell && cell.ellipsis ? 'kole-m-table__cell--ellipsis' : ''
|
||
].filter(Boolean);
|
||
}
|
||
|
||
function onRowClick(ri, row) {
|
||
if (row && row.disabled) return;
|
||
emit('select', ri, row);
|
||
}
|
||
|
||
/* 键盘路径:聚焦后按回车 / 空格选中该行(整行点击不能是唯一的交互路径,规格 §47.6) */
|
||
function onRowKey(ri, row, e) {
|
||
if (!props.clickable || (row && row.disabled)) return;
|
||
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
|
||
e.preventDefault();
|
||
emit('select', ri, row);
|
||
}
|
||
</script>
|
||
|
||
<style src="./Table.css"></style>
|
||
</code></pre>
|
||
</div>
|
||
</details>
|
||
<details class="m-details">
|
||
<summary>frameworks-mobile/Table.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 278 行</summary>
|
||
<div class="m-code">
|
||
<div class="m-code-head"><span>frameworks-mobile/Table.uniapp.vue</span><button class="m-copy" type="button" data-copy="code-270">复制</button></div>
|
||
<pre id="code-270"><code><template>
|
||
<view class="kole-m-table" :class="rootClass" :data-mode="isCard ? 'card' : 'scroll'">
|
||
<!-- 横向滚动视口:滚动只发生在这里,页面整体不横向溢出(规格 §47.1) -->
|
||
<scroll-view class="kole-m-table__viewport" :scroll-x="!isCard" scroll-y="false">
|
||
<view class="kole-m-table__inner">
|
||
<view class="kole-m-table__head">
|
||
<view class="kole-m-table__row kole-m-table__row--head">
|
||
<view
|
||
v-for="(col, ci) in cols"
|
||
:key="keyOf(col, ci)"
|
||
class="kole-m-table__head-cell"
|
||
:class="headClass(col)"
|
||
:role="'columnheader'"
|
||
>
|
||
<text>{{ col && col.title !== undefined ? col.title : '' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="rows.length" class="kole-m-table__body">
|
||
<view
|
||
v-for="(row, ri) in rows"
|
||
:key="row && row.key !== undefined ? String(row.key) : ri"
|
||
class="kole-m-table__row"
|
||
:class="{ 'is-selected': !!(row && row.selected), 'is-disabled': !!(row && row.disabled) }"
|
||
:role="'row'"
|
||
:aria-selected="clickable && !(row && row.disabled) ? (row && row.selected ? 'true' : 'false') : 'false'"
|
||
:aria-disabled="row && row.disabled ? 'true' : 'false'"
|
||
@tap="onRowTap(ri, row)"
|
||
>
|
||
<view
|
||
v-for="(col, ci) in cols"
|
||
:key="keyOf(col, ci) + '-' + ri"
|
||
class="kole-m-table__cell"
|
||
:class="cellClass(col, row, ci)"
|
||
:role="'cell'"
|
||
:data-label="col && col.title !== undefined ? String(col.title) : ''"
|
||
>
|
||
<text>{{ textOf(cellOf(row, col, ci)) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<text v-if="!rows.length" class="kole-m-table__empty">{{ emptyText }}</text>
|
||
<slot></slot>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
/* uni-app 端 · 表格(移动端)— 规格 §47
|
||
跨端差异:小程序没有原生 table,用 view 网格 + role="row" / "cell" / "columnheader" 表达语义;
|
||
横向滚动用 scroll-view(scroll-x)承担 —— 滚动只发生在视口内,页面整体不横向溢出(规格 §47.1)。
|
||
mode=card 时关掉 scroll-x,把每行摊成键值对(字段名由 data-label 生成)。
|
||
表头在卡片模式外始终渲染在内容之前(scroll-view 内不依赖 sticky:小程序 sticky 支持不一)。
|
||
点击一律 @tap;尺寸用 rpx(88rpx = 375pt 下的 44px 触控最小边长)。 */
|
||
import { computed } from 'vue';
|
||
|
||
const props = defineProps({
|
||
columns: { type: Array, default: () => [] },
|
||
rows: { type: Array, default: () => [] },
|
||
mode: { type: String, default: 'scroll' },
|
||
size: { type: String, default: 'default' },
|
||
stripe: { type: Boolean, default: false },
|
||
bordered: { type: Boolean, default: false },
|
||
clickable: { type: Boolean, default: false },
|
||
caption: { type: String, default: '' },
|
||
emptyText: { type: String, default: '暂无数据' }
|
||
});
|
||
const emit = defineEmits(['select']);
|
||
|
||
const isCard = computed(() => props.mode === 'card');
|
||
|
||
const cols = computed(() =>
|
||
(Array.isArray(props.columns) ? props.columns : []).map((c, i) =>
|
||
c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i }
|
||
)
|
||
);
|
||
|
||
const rootClass = computed(() => [
|
||
'kole-m-table--' + (isCard.value ? 'card' : 'scroll'),
|
||
props.size === 'compact' ? 'kole-m-table--compact' : '',
|
||
props.stripe ? 'kole-m-table--stripe' : '',
|
||
props.bordered ? 'kole-m-table--bordered' : '',
|
||
props.clickable ? 'kole-m-table--clickable' : ''
|
||
].filter(Boolean));
|
||
|
||
function keyOf(col, ci) {
|
||
return col && col.key !== undefined ? String(col.key) : String(ci);
|
||
}
|
||
|
||
function cellOf(row, col, ci) {
|
||
if (Array.isArray(row)) return { text: row[ci] };
|
||
if (row && typeof row === 'object') {
|
||
const raw = row[col && col.key !== undefined ? col.key : ci];
|
||
if (raw && typeof raw === 'object') return raw;
|
||
return { text: raw };
|
||
}
|
||
return { text: '' };
|
||
}
|
||
|
||
function textOf(cell) {
|
||
if (cell === null || cell === undefined) return '';
|
||
return String(cell.text === undefined ? cell : cell.text);
|
||
}
|
||
|
||
function alignOf(col, cell) {
|
||
if (cell && cell.number) return 'number';
|
||
if (cell && cell.align) return cell.align;
|
||
if (col && col.number) return 'number';
|
||
if (col && col.align) return col.align;
|
||
return 'start';
|
||
}
|
||
|
||
function headClass(col) {
|
||
if (col && col.number) return 'kole-m-table__head-cell--number';
|
||
if (col && col.align) return 'kole-m-table__head-cell--' + col.align;
|
||
return '';
|
||
}
|
||
|
||
function cellClass(col, row, ci) {
|
||
const cell = cellOf(row, col, ci);
|
||
return [
|
||
'kole-m-table__cell--' + alignOf(col, cell),
|
||
cell && cell.ellipsis ? 'kole-m-table__cell--ellipsis' : ''
|
||
].filter(Boolean);
|
||
}
|
||
|
||
function onRowTap(ri, row) {
|
||
if (row && row.disabled) return;
|
||
emit('select', ri, row);
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.kole-m-table {
|
||
--kole-m-table-row-height: 88rpx;
|
||
--kole-m-table-cell-padding: 24rpx;
|
||
--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;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
min-width: 0;
|
||
background-color: var(--kole-color-card-bg);
|
||
color: var(--kole-color-text-body);
|
||
font-size: var(--kole-m-font-size-body);
|
||
}
|
||
|
||
/* 横向滚动视口:滚动只发生在这里 */
|
||
.kole-m-table__viewport {
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.kole-m-table__inner {
|
||
display: inline-block;
|
||
min-width: 100%;
|
||
}
|
||
|
||
.kole-m-table__row {
|
||
display: flex;
|
||
align-items: stretch;
|
||
min-height: var(--kole-m-table-row-height);
|
||
}
|
||
|
||
.kole-m-table__head-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
padding: 24rpx var(--kole-m-table-cell-padding);
|
||
background-color: var(--kole-color-table-header-bg);
|
||
color: var(--kole-color-text-secondary);
|
||
font-size: var(--kole-m-font-size-label);
|
||
border-bottom: 1rpx solid var(--kole-color-border);
|
||
}
|
||
|
||
.kole-m-table__cell {
|
||
display: flex;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
padding: 24rpx var(--kole-m-table-cell-padding);
|
||
background-color: var(--kole-color-card-bg);
|
||
color: var(--kole-color-text-body);
|
||
font-size: var(--kole-m-font-size-body);
|
||
border-bottom: 1rpx solid var(--kole-color-border);
|
||
}
|
||
|
||
.kole-m-table__row:last-child .kole-m-table__cell { border-bottom: 0; }
|
||
|
||
/* 变体 size=compact:行高不变(触控下限),只收紧字号 */
|
||
.kole-m-table--compact .kole-m-table__inner { font-size: var(--kole-m-font-size-label); }
|
||
|
||
/* 列对齐 */
|
||
.kole-m-table__cell--center,
|
||
.kole-m-table__head-cell--center { justify-content: center; }
|
||
|
||
.kole-m-table__cell--end,
|
||
.kole-m-table__head-cell--end { justify-content: flex-end; }
|
||
|
||
/* 数字列:等宽字 + 右对齐,方便按位对比 */
|
||
.kole-m-table__cell--number,
|
||
.kole-m-table__head-cell--number { justify-content: flex-end; }
|
||
|
||
/* 变体 bordered=true:单元格之间也画竖线 */
|
||
.kole-m-table--bordered .kole-m-table__head-cell + .kole-m-table__head-cell,
|
||
.kole-m-table--bordered .kole-m-table__cell + .kole-m-table__cell {
|
||
border-left: 1rpx solid var(--kole-color-border);
|
||
}
|
||
|
||
/* 变体 stripe=true:偶数行浅底 */
|
||
.kole-m-table--stripe .kole-m-table__body .kole-m-table__row:nth-child(even) .kole-m-table__cell {
|
||
background-color: var(--kole-color-table-header-bg);
|
||
}
|
||
|
||
/* 状态 selected:当前选中行 */
|
||
.kole-m-table__row.is-selected .kole-m-table__cell {
|
||
background-color: var(--kole-color-brand-bg);
|
||
color: var(--kole-color-brand);
|
||
}
|
||
|
||
/* 状态 disabled:置灰且不响应 */
|
||
.kole-m-table__row.is-disabled .kole-m-table__cell { color: var(--kole-color-text-disabled); }
|
||
|
||
/* 状态 empty:空态占位 */
|
||
.kole-m-table__empty {
|
||
display: block;
|
||
padding: 80rpx var(--kole-m-gutter);
|
||
color: var(--kole-color-text-secondary);
|
||
font-size: var(--kole-m-font-size-label);
|
||
text-align: center;
|
||
border-bottom: 1rpx solid var(--kole-color-border);
|
||
}
|
||
|
||
/* 变体 mode=card:每行摊成键值对,彻底不横向滚动 */
|
||
.kole-m-table--card .kole-m-table__viewport { white-space: normal; }
|
||
|
||
.kole-m-table--card .kole-m-table__inner {
|
||
display: block;
|
||
min-width: 0;
|
||
}
|
||
|
||
.kole-m-table--card .kole-m-table__head { display: none; }
|
||
|
||
.kole-m-table--card .kole-m-table__row {
|
||
display: block;
|
||
padding: 24rpx var(--kole-m-gutter);
|
||
border-bottom: 1rpx solid var(--kole-color-border);
|
||
}
|
||
|
||
.kole-m-table--card .kole-m-table__cell {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8rpx 0;
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.kole-m-table--card .kole-m-table__cell::before {
|
||
content: attr(data-label);
|
||
color: var(--kole-color-text-secondary);
|
||
font-size: var(--kole-m-font-size-label);
|
||
}
|
||
|
||
/* 状态 clickable:整行是热区;行必须同时可聚焦(role="row" + tabindex 由宿主传入),
|
||
否则点击成了唯一的交互路径(规格 §47.6)。cursor 只给表体行,表头不变手型。 */
|
||
.kole-m-table__body .kole-m-table__row:active .kole-m-table__cell {
|
||
background-color: var(--kole-color-brand-bg);
|
||
}
|
||
</style>
|
||
</code></pre>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="tests">测试与回归</h2>
|
||
<p class="m-sub">断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 <code>tests/_runtime.js</code>,触控行为动词来自移动端 <code>tests/mobile/_behaviors.js</code>)。</p>
|
||
<p>
|
||
<span class="m-status pass">断言 18 条 · 全部通过</span>
|
||
<span class="m-status">报告 2026-09-22 23:01:05</span>
|
||
</p>
|
||
<div class="m-code">
|
||
<div class="m-code-head"><span>复现命令</span><button class="m-copy" type="button" data-copy="code-test">复制</button></div>
|
||
<pre id="code-test"><code>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 与源码一致性</code></pre>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="m-block">
|
||
<h2 id="contract">设计契约</h2>
|
||
<details class="m-details">
|
||
<summary>components/mobile-table.json(点击展开原始 JSON)</summary>
|
||
<div class="m-code"><pre><code>{
|
||
"schemaVersion": 1,
|
||
"sourceKind": "authored-spec",
|
||
"provenance": "authored-in-repo",
|
||
"specFile": "spec/移动端规格.md",
|
||
"specSection": "47 · 表格 Table",
|
||
"confidence": "high",
|
||
"slug": "mobile-table",
|
||
"name": "表格 Table",
|
||
"semanticTypeCandidates": [
|
||
"table",
|
||
"datagrid",
|
||
"list-grid"
|
||
],
|
||
"variantDimensions": [
|
||
{
|
||
"name": "mode",
|
||
"values": [
|
||
"scroll",
|
||
"card"
|
||
]
|
||
},
|
||
{
|
||
"name": "size",
|
||
"values": [
|
||
"default",
|
||
"compact"
|
||
]
|
||
},
|
||
{
|
||
"name": "stripe",
|
||
"values": [
|
||
"false",
|
||
"true"
|
||
]
|
||
},
|
||
{
|
||
"name": "bordered",
|
||
"values": [
|
||
"false",
|
||
"true"
|
||
]
|
||
}
|
||
],
|
||
"representativeVariants": [
|
||
{
|
||
"mode": "scroll",
|
||
"size": "default",
|
||
"stripe": "false",
|
||
"bordered": "true",
|
||
"label": "横向滚动(列多时在组件容器里左右滑)"
|
||
},
|
||
{
|
||
"mode": "scroll",
|
||
"size": "default",
|
||
"stripe": "true",
|
||
"bordered": "false",
|
||
"label": "斑马纹(偶数行浅底,便于对齐行)"
|
||
},
|
||
{
|
||
"mode": "card",
|
||
"size": "default",
|
||
"stripe": "false",
|
||
"bordered": "false",
|
||
"label": "卡片模式(每行摊成键值对,小屏不横滚)"
|
||
},
|
||
{
|
||
"mode": "scroll",
|
||
"size": "compact",
|
||
"stripe": "false",
|
||
"bordered": "true",
|
||
"label": "紧凑(收紧内边距与字号,行高仍 ≥44px)"
|
||
}
|
||
],
|
||
"anatomy": {
|
||
"table": "根元素,纵向排列「视口 + 空态」",
|
||
"table__viewport": "横向滚动的唯一发生点(overflow-x: auto),页面整体不横向溢出",
|
||
"table__inner": "原生 table,min-width 由 --kole-m-table-min-width 给出;超过容器即滚",
|
||
"table__caption": "可选表格标题(说明这张表在讲什么)",
|
||
"table__head": "表头,position: sticky 固定在滚动视口顶部",
|
||
"table__head-cell": "表头单元格,写 scope=col 让读屏报出列名",
|
||
"table__row": "数据行,行高不小于 44px",
|
||
"table__cell": "单元格,可带 data-label(卡片模式的字段名来源)",
|
||
"table__empty": "空态占位,role=status + aria-live=polite"
|
||
},
|
||
"structurePatterns": {
|
||
"mode": "scroll(横向滚动,列多时用)/ card(每行摊成键值对,小屏彻底不横滚)",
|
||
"size": "default(常规行高)/ compact(收紧内边距与字号;行高仍不小于 44px)",
|
||
"stripe": "false(纯白底)/ true(偶数行浅底,便于横向扫读时对齐行)",
|
||
"bordered": "false(只有横线)/ true(单元格之间也有竖线)"
|
||
},
|
||
"usageHints": [
|
||
"把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合",
|
||
"移动端表格必须在组件自己的容器里横向滚动,绝不能让页面整体横向溢出,否则整页会跟着左右晃",
|
||
"横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽",
|
||
"表头用 position: sticky 而不是 fixed —— fixed 会脱出滚动容器,横向滚动时表头不跟随",
|
||
"长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比",
|
||
"点整行即选中(同表互斥);行内的独立控件要阻止冒泡,避免一次点击触发两个动作"
|
||
],
|
||
"doNotInvent": [
|
||
"虚拟滚动与无限加载的触发规则(由宿主容器负责;本组件不接管纵向滚动)",
|
||
"服务端排序 / 筛选 / 分页(排序图标与请求都由宿主发起)",
|
||
"列宽拖拽调整与列显隐设置(移动端没有这个操作面)",
|
||
"冻结列(position: sticky 的横向版本;当前只固定表头的纵向位置)"
|
||
],
|
||
"unknowns": [
|
||
"表头是否需要在纵向滚动时也吸顶(当前只在宿主给固定高度时生效)",
|
||
"行选中是单选还是多选(当前单选;多选应交给 Checkbox 列)",
|
||
"卡片模式下字段名的排列是「左标签右值」还是「上标签下值」"
|
||
],
|
||
"interaction": [
|
||
"横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽、页面整体横向溢出",
|
||
"选项行高不小于 44px;触屏上横向滑动不被父级纵向滚动吞掉(touch-action: pan-x pan-y)",
|
||
"表头用 position: sticky 而不是 fixed——fixed 会脱出滚动容器,横向滚动时表头不跟随",
|
||
"长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比",
|
||
"点整行即选中(同表互斥);行内的独立控件(按钮 / 链接)要阻止冒泡,避免一次点击触发两个动作",
|
||
"行可点时同时提供键盘路径(聚焦后回车 / 空格),不把鼠标点击当成唯一入口"
|
||
],
|
||
"accessibility": [
|
||
"用原生 table / thead / tbody / tr / th / td,表头写 scope=\"col\"(读屏能报出列名)",
|
||
"空态用 role=\"status\" + aria-live=\"polite\" 播报占位文案",
|
||
"禁用行写 aria-disabled=\"true\",读屏播报不可用",
|
||
"可点行必须同时可聚焦:clickable=true 时行写 tabindex=\"0\" + role=\"row\" + aria-selected,聚焦后回车 / 空格即选中 —— 只有鼠标点击而没有键盘路径是缺陷,不是简化",
|
||
"焦点环画在行内第一个单元格上(tr 上的 outline 在部分浏览器不渲染)",
|
||
"mode=card 时字段名由 data-label 生成并参与读屏(键值对在视觉与语义上都成立)"
|
||
],
|
||
"api": {
|
||
"source": "implementation",
|
||
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
|
||
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
|
||
"props": [
|
||
{
|
||
"name": "columns",
|
||
"type": "Array<{ title: string, key?: string, align?: 'start'|'center'|'end', number?: boolean }>",
|
||
"default": "[]",
|
||
"desc": "列定义(宿主提供;number 是数字列的便捷写法,等价于 align=end + 等宽字)(规格 §47.3)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "rows",
|
||
"type": "Array<Array | { key?: string, selected?: boolean, disabled?: boolean }>",
|
||
"default": "[]",
|
||
"desc": "行数据,二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }(规格 §47.2)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "mode",
|
||
"type": "'scroll' | 'card'",
|
||
"default": "'scroll'",
|
||
"desc": "变体 mode:scroll 横向滚动,card 每行摊成键值对(规格 §47.3)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "size",
|
||
"type": "'default' | 'compact'",
|
||
"default": "'default'",
|
||
"desc": "变体 size:compact 收紧内边距与字号,行高仍不小于 44px(规格 §47.3)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "stripe",
|
||
"type": "boolean",
|
||
"default": "false",
|
||
"desc": "变体 stripe:偶数行浅底,便于横向扫读时对齐行(规格 §47.3)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "bordered",
|
||
"type": "boolean",
|
||
"default": "false",
|
||
"desc": "变体 bordered:单元格之间也画竖线(规格 §47.3)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "clickable",
|
||
"type": "boolean",
|
||
"default": "false",
|
||
"desc": "状态 clickable:整行可点,按下反馈是整行背景变化(规格 §47.4)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "caption",
|
||
"type": "string",
|
||
"default": "''",
|
||
"desc": "可选表格标题(说明这张表在讲什么)(规格 §47.2)",
|
||
"required": false
|
||
},
|
||
{
|
||
"name": "emptyText",
|
||
"type": "string",
|
||
"default": "'暂无数据'",
|
||
"desc": "状态 empty 的占位文案(规格 §47.4)",
|
||
"required": false
|
||
}
|
||
],
|
||
"events": [
|
||
{
|
||
"name": "select",
|
||
"params": "index: number, row: any",
|
||
"desc": "点整行时触发,回传行下标与该行数据(clickable=true 时才有意义)(规格 §47.5)"
|
||
}
|
||
],
|
||
"slots": [
|
||
{
|
||
"name": "default",
|
||
"desc": "自定义内容(追加在空态之后)"
|
||
}
|
||
]
|
||
},
|
||
"variantClasses": {
|
||
"mode": {
|
||
"scroll": [
|
||
".kole-m-table--scroll"
|
||
],
|
||
"card": [
|
||
".kole-m-table--card"
|
||
]
|
||
},
|
||
"size": {
|
||
"default": [],
|
||
"compact": [
|
||
".kole-m-table--compact"
|
||
]
|
||
},
|
||
"stripe": {
|
||
"false": [],
|
||
"true": [
|
||
".kole-m-table--stripe"
|
||
]
|
||
},
|
||
"bordered": {
|
||
"false": [],
|
||
"true": [
|
||
".kole-m-table--bordered"
|
||
]
|
||
}
|
||
},
|
||
"demos": [
|
||
{
|
||
"id": "scroll",
|
||
"group": "01 组件类型",
|
||
"title": "横向滚动",
|
||
"desc": "列多时在表格自己的容器里左右滑,页面整体不横向溢出。",
|
||
"variant": "mode=scroll"
|
||
},
|
||
{
|
||
"id": "clickable",
|
||
"group": "02 组件状态",
|
||
"title": "表头固定与行点击",
|
||
"desc": "点整行选中该行;数字列等宽右对齐便于按位对比,斑马纹便于横向对齐行。",
|
||
"variant": "状态 selected | clickable"
|
||
},
|
||
{
|
||
"id": "card",
|
||
"group": "01 组件类型",
|
||
"title": "卡片模式",
|
||
"desc": "mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动。",
|
||
"variant": "mode=card"
|
||
},
|
||
{
|
||
"id": "empty",
|
||
"group": "02 组件状态",
|
||
"title": "空态",
|
||
"desc": "没有数据时显示占位文案,列宽语义不变,不会把容器撑宽。",
|
||
"variant": "状态 empty"
|
||
},
|
||
{
|
||
"id": "states",
|
||
"group": "02 组件状态",
|
||
"title": "禁用行与省略列",
|
||
"desc": "禁用行置灰不响应;长文本列单行省略,完整值在 title 里。",
|
||
"variant": "状态 disabled"
|
||
}
|
||
],
|
||
"related": [
|
||
{
|
||
"slug": "mobile-list",
|
||
"why": "一行只需「标题 + 值 + 箭头」时用列表,不需要列对齐;列对比才是表格的强项"
|
||
},
|
||
{
|
||
"slug": "cell",
|
||
"why": "键值对数量很少(三四项)时用单元格纵向排列,比建一张两列表格更轻"
|
||
},
|
||
{
|
||
"slug": "mobile-collapse",
|
||
"why": "需要展开某一行看明细时用折叠面板承载详情,表格本身不做行内展开"
|
||
},
|
||
{
|
||
"slug": "mobile-loading",
|
||
"why": "表格数据加载中用它占位,不要在表体里自己画骨架屏"
|
||
},
|
||
{
|
||
"slug": "mobile-tag",
|
||
"why": "状态列用小标签呈现(已发货 / 待付款)比纯文字更好扫读"
|
||
}
|
||
]
|
||
}</code></pre></div>
|
||
</details>
|
||
</section>
|
||
|
||
<footer class="m-foot">
|
||
<p>生成物:本页由 <code>node tools/build-mobile.mjs</code> 从契约 <code>components/mobile-table.json</code> 与 6 端源码生成,请勿手改。</p>
|
||
<p>规格原文:<code>.design_library/kole-ui-mobile/spec/移动端规格.md</code> · 隔离门禁:<code>node tools/verify-mobile-isolation.mjs</code></p>
|
||
</footer>
|
||
</main>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
/* 左栏:≤1000px 默认收起(手机上不让导航占满一屏),跨断点时同步一次;不监听 resize,
|
||
免得跟用户手动的展开/收起抢状态。桌面不设 open 之外的动作。 */
|
||
var side = document.querySelector('details.m-side');
|
||
if (side && window.matchMedia) {
|
||
var mq = window.matchMedia('(max-width: 1000px)');
|
||
var sync = function () { if (mq.matches) side.removeAttribute('open'); else side.setAttribute('open', ''); };
|
||
sync();
|
||
if (mq.addEventListener) mq.addEventListener('change', sync);
|
||
else if (mq.addListener) mq.addListener(sync);
|
||
}
|
||
/* 单演示预览帧按内容高度自适应(同源可读 contentDocument;上限 360px、下限 140px)。
|
||
固定高度会在内容少的演示上留一大片空白(实测 SwipeCell 单个演示只用 ~120px)。 */
|
||
function fitDemoFrames() {
|
||
var frames = document.querySelectorAll('.m-demo-stage iframe');
|
||
Array.prototype.forEach.call(frames, function (f) {
|
||
var fit = function () {
|
||
try {
|
||
var d = f.contentDocument;
|
||
if (!d || !d.body) return;
|
||
/* 只量 body.scrollHeight:documentElement.scrollHeight 等于**帧视口高度**(html 撑满视口),
|
||
拿它做 max 会形成「帧高→视口高→量到的高度」反馈环,永远收敛在初始值(实测锁在 308px)。 */
|
||
var h = d.body.scrollHeight;
|
||
if (!h) h = d.documentElement ? d.documentElement.scrollHeight : 0;
|
||
if (!h) return;
|
||
f.style.height = Math.min(360, Math.max(140, h + 8)) + 'px';
|
||
} catch (e) { /* 跨源或未就绪:保持 CSS 兜底高度 */ }
|
||
};
|
||
f.addEventListener('load', function () { fit(); setTimeout(fit, 300); });
|
||
fit();
|
||
});
|
||
}
|
||
fitDemoFrames();
|
||
window.addEventListener('load', fitDemoFrames);
|
||
/* 主题模式(三态):与 PC 完全同一约定 —— localStorage[kole-mode] ∈ light|dark|auto,
|
||
auto 跟随系统,反色靠 html.kole-dark(令牌文件里已写好该组,两端共用一份令牌)。
|
||
用户在 PC 站选夜间,进移动端站也是夜间。 */
|
||
var MODE_KEY = 'kole-mode';
|
||
var MODE_LABEL = { light: '日间模式', dark: '夜间模式', auto: '自动模式' };
|
||
function readMode() {
|
||
try { var v = localStorage.getItem(MODE_KEY); if (v === 'light' || v === 'dark' || v === 'auto') return v; } catch (e) {}
|
||
return 'auto';
|
||
}
|
||
function systemDark() {
|
||
return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||
}
|
||
function applyMode(mode) {
|
||
var dark = mode === 'dark' || (mode === 'auto' && systemDark());
|
||
document.documentElement.classList.toggle('kole-dark', dark);
|
||
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
|
||
var cur = document.getElementById('m-mode-cur');
|
||
if (cur) cur.textContent = MODE_LABEL[mode] || MODE_LABEL.auto;
|
||
Array.prototype.forEach.call(document.querySelectorAll('.m-menu-opt'), function (o) {
|
||
o.setAttribute('aria-selected', o.getAttribute('data-mode') === mode ? 'true' : 'false');
|
||
});
|
||
/* 三态图标与 PC 同口径:显式选择时按时态给图标(日间=月亮 / 夜间=太阳),
|
||
自动模式下恒为「跟随系统」的显示器图标(此时两个显式图标都不显示)。 */
|
||
var moon = document.getElementById('m-icon-moon');
|
||
var sun = document.getElementById('m-icon-sun');
|
||
var auto = document.getElementById('m-icon-auto');
|
||
if (moon) moon.style.display = (mode === 'light') ? '' : 'none';
|
||
if (sun) sun.style.display = (mode === 'dark') ? '' : 'none';
|
||
if (auto) auto.style.display = (mode === 'auto') ? '' : 'none';
|
||
}
|
||
function bindModeMenu() {
|
||
var trigger = document.getElementById('m-mode-trigger');
|
||
var menu = document.getElementById('m-mode-menu');
|
||
if (!trigger || !menu) return;
|
||
var close = function (refocus) {
|
||
menu.classList.add('hidden'); trigger.setAttribute('aria-expanded', 'false');
|
||
if (refocus) trigger.focus();
|
||
};
|
||
trigger.addEventListener('click', function () {
|
||
var open = menu.classList.toggle('hidden');
|
||
trigger.setAttribute('aria-expanded', open ? 'false' : 'true');
|
||
});
|
||
trigger.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape') close(true);
|
||
});
|
||
Array.prototype.forEach.call(menu.querySelectorAll('.m-menu-opt'), function (opt) {
|
||
opt.addEventListener('click', function () {
|
||
var mode = opt.getAttribute('data-mode');
|
||
try { localStorage.setItem(MODE_KEY, mode); } catch (e) {}
|
||
applyMode(mode);
|
||
close(true);
|
||
});
|
||
});
|
||
document.addEventListener('click', function (e) {
|
||
if (!menu.classList.contains('hidden') && !menu.contains(e.target) && !trigger.contains(e.target)) close(false);
|
||
});
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && !menu.classList.contains('hidden')) close(true);
|
||
});
|
||
/* 自动模式下跟随系统切换 */
|
||
if (window.matchMedia) {
|
||
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||
var onChange = function () { if (readMode() === 'auto') applyMode('auto'); };
|
||
if (mq.addEventListener) mq.addEventListener('change', onChange);
|
||
else if (mq.addListener) mq.addListener(onChange);
|
||
}
|
||
applyMode(readMode());
|
||
}
|
||
bindModeMenu();
|
||
/* 版本下拉:与 PC 顶栏同一实现口径 —— 清单从部署根取(<prefix>/versions.json 与
|
||
<prefix>/site/versions.json 两份按序尝试,取有可用条目的那份),拿不到就退回不可点的
|
||
纯角标(data-single="1"),不留一个点不开的下拉。
|
||
清单不许把用户带去外站:path 只允许 ".."(站点根自身)或 x.y.z 快照目录名。 */
|
||
var VERSION_KEY_PATH = '..';
|
||
function isValidVersionPath(p) {
|
||
if (typeof p !== 'string' || !p.length || p.indexOf('\\') >= 0 || p.indexOf('//') >= 0) return false;
|
||
if (p === VERSION_KEY_PATH) return true;
|
||
return /^\d+\.\d+\.\d+$/.test(p);
|
||
}
|
||
/* 清单里的 path 相对部署根,".." = /site/、x.y.z = /x.y.z/site/。
|
||
当前页可能不在部署根下(快照页),故先还原部署根绝对路径再算回相对当前页。 */
|
||
function versionHref(absSite) {
|
||
var i = location.pathname.indexOf('/site/');
|
||
var prefix = i >= 0 ? location.pathname.slice(0, i) : '';
|
||
if (!prefix || absSite.indexOf(prefix + '/') !== 0) return absSite;
|
||
var rest = absSite.slice(prefix.length + 1);
|
||
var up = '';
|
||
var segs = prefix.slice(1).split('/');
|
||
for (var k = 0; k < segs.length; k++) up += "../";
|
||
return up + rest;
|
||
}
|
||
function buildVersionList(doc) {
|
||
var raw = doc && doc.versions;
|
||
if (!raw || !raw.length) return [];
|
||
var i = location.pathname.indexOf('/site/');
|
||
var prefix = i >= 0 ? location.pathname.slice(0, i) : '';
|
||
var list = [];
|
||
for (var k = 0; k < raw.length; k++) {
|
||
var v = raw[k] || {};
|
||
var ver = v.version ? String(v.version) : '';
|
||
var path = v.path ? String(v.path) : '';
|
||
if (!ver || !isValidVersionPath(path)) continue;
|
||
var absSite = (path === VERSION_KEY_PATH ? '' : '/' + path) + '/site/';
|
||
list.push({ version: ver, date: v.date ? String(v.date) : '', latest: !!v.latest, href: versionHref(absSite), absSite: absSite, current: false });
|
||
}
|
||
if (list.length) {
|
||
/* 当前版本 = 绝对路径上命中的最长前缀(最具体的那个)——与 PC 的判定一致,
|
||
快照页与根站点都能算对。 */
|
||
var here = location.pathname;
|
||
var best = -1;
|
||
for (var j = 0; j < list.length; j++) {
|
||
if (here.indexOf(list[j].absSite) === 0 && list[j].absSite.length > best) best = list[j].absSite.length;
|
||
}
|
||
for (var m = 0; m < list.length; m++) list[m].current = best >= 0 && list[m].absSite.length === best;
|
||
if (best < 0 && list[0]) list[0].current = true;
|
||
}
|
||
return list;
|
||
}
|
||
function bindVersionMenu() {
|
||
var trigger = document.getElementById('m-ver-trigger');
|
||
var menu = document.getElementById('m-ver-menu');
|
||
var chip = document.getElementById('m-ver');
|
||
if (!trigger || !menu) return;
|
||
var close = function (refocus) {
|
||
menu.classList.add('hidden'); trigger.setAttribute('aria-expanded', 'false');
|
||
if (refocus) trigger.focus();
|
||
};
|
||
var interactive = false;
|
||
var render = function (list) {
|
||
interactive = !!(list && list.length);
|
||
/* 只有清单拿不到才退回纯角标;只要有一版就是下拉(里面至少能看清当前是哪个版本) */
|
||
trigger.setAttribute('data-single', interactive ? '0' : '1');
|
||
if (!interactive) { menu.classList.add("hidden"); return; }
|
||
var cur = null;
|
||
for (var k = 0; k < list.length; k++) if (list[k].current) cur = list[k];
|
||
if (chip) chip.textContent = 'v' + ((cur && cur.version) || VERSION);
|
||
/* 重建选项(幂等:重复调用不会叠加) */
|
||
Array.prototype.slice.call(menu.querySelectorAll('.m-ver-opt')).forEach(function (n) { n.remove(); });
|
||
list.forEach(function (it) {
|
||
var opt = document.createElement('button');
|
||
opt.type = 'button'; opt.className = 'm-ver-opt'; opt.setAttribute('role', 'option');
|
||
opt.setAttribute('data-version', it.version);
|
||
opt.setAttribute('aria-selected', it.current ? 'true' : 'false');
|
||
var tag = it.current ? '当前浏览' : (it.latest ? '最新版' : (it.date || ''));
|
||
opt.innerHTML = '<span class="m-ver-opt-num">v' + it.version + '</span><span class="m-ver-opt-tag">' + tag + '</span>';
|
||
if (it.href) opt.setAttribute('data-href', it.href);
|
||
opt.addEventListener("click", function () {
|
||
if (!it.current && it.href) location.href = it.href;
|
||
else close(true);
|
||
});
|
||
menu.appendChild(opt);
|
||
});
|
||
};
|
||
trigger.addEventListener('click', function () {
|
||
if (!interactive) return;
|
||
if (menu.classList.contains('hidden')) { menu.classList.remove('hidden'); trigger.setAttribute('aria-expanded', 'true'); }
|
||
else close(false);
|
||
});
|
||
trigger.addEventListener('keydown', function (e) {
|
||
if (!interactive) return;
|
||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); menu.classList.remove('hidden'); trigger.setAttribute('aria-expanded', 'true'); }
|
||
else if (e.key === 'Escape') close(true);
|
||
});
|
||
document.addEventListener('click', function (e) {
|
||
if (!menu.classList.contains('hidden') && !menu.contains(e.target) && !trigger.contains(e.target)) close(false);
|
||
});
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && !menu.classList.contains('hidden')) close(true);
|
||
});
|
||
/* 两份清单按序尝试(不并行:快照页那份必然 404,并行会把 404 记进控制台,
|
||
而站点门禁的「零控制台错误」是硬断言)。 */
|
||
if (!window.fetch || location.protocol === 'file:') return;
|
||
var i = location.pathname.indexOf('/site/');
|
||
if (i < 0) return;
|
||
var prefix = location.pathname.slice(0, i);
|
||
var urls = [prefix + '/versions.json', prefix + '/site/versions.json'];
|
||
var attempt = function (idx) {
|
||
if (idx >= urls.length) return;
|
||
fetch(urls[idx], { cache: 'no-store' })
|
||
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||
.then(function (doc) { var list = buildVersionList(doc); if (list.length) render(list); else attempt(idx + 1); })
|
||
.catch(function () { attempt(idx + 1); });
|
||
};
|
||
attempt(0);
|
||
}
|
||
bindVersionMenu();
|
||
function done(btn) {
|
||
var old = btn.textContent; btn.textContent = '已复制'; btn.classList.add('is-done');
|
||
setTimeout(function () { btn.textContent = old; btn.classList.remove('is-done'); }, 1200);
|
||
}
|
||
document.addEventListener('click', function (e) {
|
||
var btn = e.target && e.target.closest ? e.target.closest('.m-copy') : null;
|
||
if (!btn) return;
|
||
var src = document.getElementById(btn.getAttribute('data-copy') || '');
|
||
var text = src ? (src.innerText || src.textContent || '') : '';
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(function () { done(btn); }, function () { fallback(btn, text); });
|
||
} else { fallback(btn, text); }
|
||
});
|
||
function fallback(btn, text) {
|
||
try {
|
||
var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', '');
|
||
ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta);
|
||
ta.select(); document.execCommand("copy"); document.body.removeChild(ta); done(btn);
|
||
} catch (err) { btn.textContent = '复制失败'; }
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|