Files
aurora-admin/site/m/faq.html
T
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

462 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='24'%20height='24'%20rx='6'%20fill='%232F54EB'/%3E%3Cg%20fill='none'%20stroke='%23FFFFFF'%20stroke-width='2.25'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M7%205.5V18.5'/%3E%3Cpath%20d='M17%205.5l-6.25%204.06'/%3E%3Cpath%20d='M10.75%2014.44L17%2018.5'/%3E%3C/g%3E%3C/svg%3E">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1C1F26" media="(prefers-color-scheme: dark)">
<title>常见问题 — Kole UI Mobile</title>
<meta name="description" content="Kole UI 移动端常见问题:与 PC 端的关系、令牌引入、单位与安全区、手势事件、uni-app 端限制、多实例同一页的定位、版本与回归口径。">
<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" aria-hidden="true"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 5.5V18.5"/><path d="M17 5.5l-6.25 4.06"/><path d="M10.75 14.44L17 18.5"/></svg></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">组件总览</a>
<a href="guide.html">快速开始</a>
<a href="design.html">设计规范</a>
<a href="faq.html" class="active" aria-current="page">常见问题</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 is-active" href="faq.html" aria-current="page">常见问题</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="component/mobile-button.html">按钮<em>Button</em></a>
<a class="m-side-link" href="component/mobile-divider.html">分割线<em>Divider</em></a>
<a class="m-side-link" href="component/mobile-icon.html">图标<em>Icon</em></a>
<a class="m-side-link" href="component/mobile-layout.html">布局<em>Layout</em></a>
<a class="m-side-link" href="component/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="component/navbar.html">顶部导航栏<em>NavBar</em></a>
<a class="m-side-link" href="component/tabbar.html">底部标签栏<em>TabBar</em></a>
<a class="m-side-link" href="component/mobile-grid.html">宫格<em>Grid</em></a>
<a class="m-side-link" href="component/mobile-steps.html">步骤条<em>Steps</em></a>
<a class="m-side-link" href="component/mobile-segmented.html">分段器<em>Segmented</em></a>
<a class="m-side-link" href="component/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="component/mobile-numberkeyboard.html">数字键盘<em>NumberKeyboard</em></a>
<a class="m-side-link" href="component/mobile-datepicker.html">日期选择器<em>DatePicker</em></a>
<a class="m-side-link" href="component/mobile-input.html">输入框<em>Input</em></a>
<a class="m-side-link" href="component/mobile-search.html">搜索框<em>Search</em></a>
<a class="m-side-link" href="component/mobile-stepper.html">步进器<em>Stepper</em></a>
<a class="m-side-link" href="component/mobile-switch.html">开关<em>Switch</em></a>
<a class="m-side-link" href="component/mobile-textarea.html">多行文本框<em>Textarea</em></a>
<a class="m-side-link" href="component/mobile-checkbox.html">多选框<em>Checkbox</em></a>
<a class="m-side-link" href="component/mobile-form.html">表单<em>Form</em></a>
<a class="m-side-link" href="component/mobile-radio.html">单选框<em>Radio</em></a>
<a class="m-side-link" href="component/mobile-rate.html">评分<em>Rate</em></a>
<a class="m-side-link" href="component/mobile-slider.html">滑动选择器<em>Slider</em></a>
<a class="m-side-link" href="component/mobile-cascader.html">级联选择器<em>Cascader</em></a>
<a class="m-side-link" href="component/mobile-colorpicker.html">颜色选择器<em>ColorPicker</em></a>
<a class="m-side-link" href="component/mobile-picker.html">选择器<em>Picker</em></a>
<a class="m-side-link" href="component/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="component/cell.html">单元格<em>Cell</em></a>
<a class="m-side-link" href="component/mobile-badge.html">徽标<em>Badge</em></a>
<a class="m-side-link" href="component/mobile-tag.html">标签<em>Tag</em></a>
<a class="m-side-link" href="component/mobile-avatar.html">头像<em>Avatar</em></a>
<a class="m-side-link" href="component/mobile-list.html">列表<em>List</em></a>
<a class="m-side-link" href="component/mobile-collapse.html">折叠面板<em>Collapse</em></a>
<a class="m-side-link" href="component/mobile-table.html">表格<em>Table</em></a>
<a class="m-side-link" href="component/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="component/actionsheet.html">动作面板<em>ActionSheet</em></a>
<a class="m-side-link" href="component/pullrefresh.html">下拉刷新<em>PullRefresh</em></a>
<a class="m-side-link" href="component/swipecell.html">滑动单元格<em>SwipeCell</em></a>
<a class="m-side-link" href="component/mobile-popup.html">弹出层<em>Popup</em></a>
<a class="m-side-link" href="component/mobile-toast.html">轻提示<em>Toast</em></a>
<a class="m-side-link" href="component/mobile-dialog.html">对话框<em>Dialog</em></a>
<a class="m-side-link" href="component/mobile-noticebar.html">通知栏<em>NoticeBar</em></a>
<a class="m-side-link" href="component/mobile-loading.html">加载<em>Loading</em></a>
<a class="m-side-link" href="component/mobile-progress.html">进度条<em>Progress</em></a>
<a class="m-side-link" href="component/mobile-message.html">消息通知<em>Message</em></a>
<a class="m-side-link" href="component/mobile-overlay.html">遮罩层<em>Overlay</em></a>
<a class="m-side-link" href="component/mobile-popover.html">弹出气泡<em>Popover</em></a>
</nav>
</details>
<main class="m-main">
<section class="m-hero">
<h1>常见问题</h1>
<p>按「接入 → 令牌 → 触控 → uni-app → 排障 → 口径」六组整理。每条都对应仓库里可复现的实测或门禁断言,不是凭印象写的经验。</p>
</section>
<section class="m-block">
<h2 id="start">接入</h2>
<table class="m-table">
<thead><tr><th>问题</th><th>回答</th></tr></thead>
<tbody>
<tr><td>组件渲染出来了,但没有样式</td><td>没引令牌或组件样式。最小集合是 <code>kole-ui/mobile/tokens.css</code> + <code>kole-ui/mobile/components/index.css</code>(按需则引单个组件 CSS)。</td></tr>
<tr><td>能直接拷走一个文件用吗</td><td>能。<code>frameworks-mobile/&lt;Prefix&gt;.html</code> 是零依赖演示页(样式内联 + 原生脚本),拷进工程即可跑;<code>.css</code> 是独立的样式表。</td></tr>
<tr><td>为什么没有 PC 的那些组件</td><td>移动端与 PC 是两套清单、物理隔离(见 <a href="platform.html">平台与端</a>)。移动端只做触屏优先的组件,B 端中后台的那 103 个在 <a href="../index.html">PC 文档站</a>。</td></tr>
<tr><td>类名为什么带 <code>-m-</code></td><td><code>kole-m-</code> 是移动端命名空间(PC 是 <code>kole-</code>)。两端样式表各引各的,交叉引用会拿到未定义的样式(门禁 <code>verify:isolation</code> 会拦这类混用)。</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="tokens">令牌</h2>
<table class="m-table">
<thead><tr><th>问题</th><th>回答</th></tr></thead>
<tbody>
<tr><td><code>--kole-m-*</code> 取不到值</td><td>令牌层没加载。它在 PC 令牌文件之上加了一层,<b>不要</b>只引 PC 的 <code>colors_and_type.css</code>;引移动端的 <code>tokens.css</code>(已 @import PC 令牌)或直接引移动端令牌层。</td></tr>
<tr><td>改了颜色,移动端为什么跟着变</td><td>这是设计如此:颜色 / 字体 / 圆角 / 阴影令牌两端同源,移动端只加 <code>--kole-m-*</code>(触控尺寸、安全区、字号、动效)。改 PC 令牌文件,两端同时生效。</td></tr>
<tr><td>暗色模式要单独写一套样式吗</td><td>不用。给根元素加 <code>html.kole-dark</code> 即可(令牌文件里已写好该组);本站在顶栏「主题模式」里切换,约定与 PC 完全一致 —— 三态存 <code>localStorage['kole-mode']</code>,<code>auto</code> 跟随系统。</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="touch">触控</h2>
<table class="m-table">
<thead><tr><th>问题</th><th>回答</th></tr></thead>
<tbody>
<tr><td>安全区没生效(刘海 / 底部横条)</td><td>页面需要 <code>viewport-fit=cover</code>(演示页都带)。令牌层用 <code>@supports (padding-top: env(safe-area-inset-top))</code> 探测后再取 <code>env()</code>,不支持时为 <code>0px</code>。</td></tr>
<tr><td>滑动 / 下拉在真机上不触发</td><td>浏览器端组件用 Pointer Events;uni-app 端用 touch 事件。若你的容器截了事件(如自带手势库),需要放行;另外纵向位移更大时组件会主动让位给页面滚动。</td></tr>
<tr><td>同一页多个 <code>position: fixed</code> 组件叠在一起</td><td>这是浏览器行为(都相对视口定位)。做演示或预览时给容器加 <code>transform: translateZ(0)</code> 建立包含块,组件 CSS 不必改 —— 本站的 TabBar / ActionSheet 演示就是这么做。</td></tr>
<tr><td>点击热区多大才合规</td><td>44×44(WCAG 2.5.5 / Apple HIG),令牌是 <code>--kole-m-touch-target: 44px</code>;uni-app 端为 <code>88rpx</code>(750rpx = 视口宽度)。</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="uniapp">uni-app</h2>
<table class="m-table">
<thead><tr><th>问题</th><th>回答</th></tr></thead>
<tbody>
<tr><td>为什么 uni-app 端没有 <code>document</code> / PointerEvent</td><td>小程序与 App 端没有 DOM,也没有 PointerEvent。所以该端只用 uni 基础组件(<code>view</code> / <code>text</code>)+ <code>@touch*</code> 事件;<code>verify:uniapp</code> 会静态拦住 DOM API 与 pointer 事件。</td></tr>
<tr><td>uni-app 端为什么用 <code>rpx</code></td><td><code>rpx</code> 是移动端语义(750rpx = 视口宽度),该端在组件内用同名令牌覆盖成 rpx 值;PC × uni-app 端反过来保持 <code>px</code>。</td></tr>
<tr><td>uni-app 端真的编译过吗</td><td><b>没有</b>。本仓库只做静态门禁(9 条:SFC 结构 / <code>node --check</code> 语法 / 禁 DOM / 手势必须 touch / 前缀隔离 / 单位策略),真实编译需要 <code>@dcloudio</code> 依赖,复现命令在 <code>tools/verify-uniapp.mjs</code> 头部(ROADMAP S7-P25)。</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="debug">排障</h2>
<table class="m-table">
<thead><tr><th>现象</th><th>怎么办</th></tr></thead>
<tbody>
<tr><td>改了索引但页面没变</td><td>本站是生成物:跑 <code>npm run build:mobile</code> 重建(CI 里有 <code>git diff --quiet -- site/m tests/mobile</code> 断言,防止「改了索引忘了重建」)。</td></tr>
<tr><td>本地打开是 403 / 404</td><td>dev-server 白名单没有该顶层目录(要加 <code>PUBLIC_ROOTS</code> 与 <code>isPublicPath()</code> 两处);或 3311 上还挂着改动前启动的实例 —— 现在会自动回退端口并打印实际入口。</td></tr>
<tr><td>演示帧里样式全丢</td><td>相对路径层数不对(深层页要用三级 <code>../../../</code> 回到仓库根)。这是实测踩过的坑,已由 <code>verify:mobile-site</code>(浏览器实测)卡住。</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="scope">口径</h2>
<table class="m-table">
<thead><tr><th>问题</th><th>回答</th></tr></thead>
<tbody>
<tr><td>版本号以什么为准</td><td><code>package.json</code> 的 <code>version</code>,构建时写进 <code>site/m/data.mobile.json</code> 的 <code>meta.version</code>(顶栏角标读它)。</td></tr>
<tr><td>回归口径</td><td>移动端断言跑在真实 375×640 设备帧里(引擎与 PC 共用 <code>tests/_runtime.js</code>,触控动词在 <code>tests/mobile/_behaviors.js</code>);报告 <code>tests/mobile-report.json</code> 含逐页结果。跑法:<code>node tools/run-mobile-regression.mjs</code>。</td></tr>
<tr><td>规格从哪来</td><td>移动端<b>没有</b>外部规范原文,规格由本仓库撰写:<code>.design_library/kole-ui-mobile/spec/移动端规格.md</code>。契约 JSON 的 <code>sourceKind</code> 是 <code>authored-spec</code>、<code>provenance</code> 是 <code>authored-in-repo</code>。</td></tr>
</tbody>
</table>
</section>
<footer class="m-foot">
<p>生成物:本页由 <code>node tools/build-mobile.mjs</code> 生成,请勿手改。</p>
<p>下一步:<a href="guide.html">快速开始</a> · <a href="design.html">设计规范</a> · <a href="platform.html">平台与端</a></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>