Skip to content

作者

novlan1

2026.1.1

TDesign 碎片笔记

harmony 中 countdown 中 cppcrash

2026-06-29

重新分析

关键事实

  1. 崩溃时 Process life time: 20s,进入前台 15 秒就崩;
  2. Page switch history 显示没有页面销毁过(Page:2、Page:3 都还活跃,只是切到 count-down 页时叠在第 3 页);
  3. 堆栈是 Builtins_ProxySetPropertySetKeyedPropertyJumpIfToBooleanFalseHandler 时崩溃;
  4. demo 页同时开了 20 多个 count-down 实例,其中 6 个是 millisecond 模式(每 33ms 一次 setTimeout 写 reactive 数据)
  5. 寄存器 x0 是 0x6b6b3a18de993c2f,被 GC sweep 标记污染过的指针特征。

真正的根因:不是销毁后回调,而是高频 setTimeout 33ms 写 reactive proxy + 鸿蒙 V8 GC 的 race,每个组件每秒 ~30 次 set,6 个 millisecond 实例每秒 180 次写入 reactive,叠加 14 个普通秒级实例 14 次/s,总计 ~200 次/s 的 reactive set。鸿蒙 V8 的 ProxySetProperty 在 GC 期间会读到无效指针。

所以正确的修复方向是

  • 降低写频率(毫秒模式从 33ms 降到 100ms 或以上);
  • 减少每次写的 reactive 字段数(合并写、减少 emit);
  • 关键:毫秒模式下不要每次都重建 timeData / timeRange / formattedTime 这些 reactive 对象,只在需要时才更新。

我先检查一下 parseTimeDataparseFormatuniComponent 包装:

好,现在分析得更清楚了。每次 updateTime 都会:

  1. 创建新对象 parseTimeData(remain) 并赋给 this.timeData(reactive set,子属性 5 个);
  2. 创建新数组 format.split(':') 赋给 this.timeRange(reactive set);
  3. 修改 this.formattedTime(reactive set);
  4. 修改 this.remain(reactive set);
  5. $emit('change', timeData) —— 走 Vue 事件系统。

毫秒模式下每 33ms 一轮,6 个毫秒实例 × 5+ 个 reactive set/轮 = 每秒近 1000 次 reactive proxy.set,鸿蒙 V8 在这种压力下会触发 GC race。

修复方案

我做以下几件事:

  1. 降低 millisecond 模式频率:从 33ms 改为 100ms(人眼 10fps 已经够看了);
  2. 避免不必要的 reactive 写入
    • this.timeRange 不是数据,不需要响应式更新——它由 format 决定,且 format 是 prop。可以放进 computed 或 mounted 里只算一次,避免每帧重写;
    • parseTimeData 返回的对象只在毫秒模式才需要每帧更新,普通秒模式只在秒变化时改;
    • 减少 $emit('change') 频率:和 updateTime 的 reactive 更新一起,按秒触发即可(毫秒模式继续保持每帧)。
  3. 使用 Object.freeze 包裹 timeData:让 Vue3 不要给内部 5 个属性加 reactive proxy(每次都是新对象,没必要做深响应式,节省大量 Proxy.set)。
  4. 保留之前的 _destroyed 守卫——这个本身没坏处,是合理的防御。

另外,由于鸿蒙 demo 页同时挂载了 6 个 millisecond 实例 + 14 个普通实例,即便单实例频率降低,累加并发还是不小。我建议同时修改 demo 页,让 millisecond 实例减少 / 或加 v-if 控制 —— 但这是 demo 测试页不能动。我们再加一道保险:所有实例共用同一个全局 ticker

不过这会改组件 API。先做最小修改试一下,如果还崩再加全局 ticker。

我再看一眼 uniComponent 包装,确认它对 data 做了什么:

这次先到这里,等用户反馈。我也再读一次最新文件确认改动正确:

重新分析的结论

上次诊断有误

之前判断"销毁后回调访问 reactive 实例"导致 V8 SIGSEGV,但新崩溃日志推翻了这个假设

  • Process life time: 20s,进入前台 15 秒就崩;
  • Page switch history 显示 Page:2/Page:3 都还在,没有发生过页面销毁
  • 崩溃时只是停留在 count-down demo 页里待着。

真正的根因

崩溃堆栈链路:

JumpIfToBooleanFalseHandler  ← 崩溃点 (访问 0x6b6b3a18de993c2f 非法地址)
InterpreterEntryTrampoline
ProxySetProperty             ← Vue3 reactive Proxy 的 set 陷阱
SetNamedPropertyHandler

寄存器 x0:6b6b3a18de993c2f 是被 GC sweep 标记污染过的指针。

CountDown demo 页同时挂载了 20 多个 count-down 实例,其中 6 个开启 millisecond 模式:每 33ms 一次 setTimeout 回调,每次会做 5+ 次 reactive set(remain/timeData/timeRange/formattedTime/emit change),叠加后 每秒接近 1000 次 Proxy.set。鸿蒙端 V8 在这个频率下,主线程 Proxy.set 与 GC 标记线程 race,命中已被 GC 移动 / 释放的对象,导致 SIGSEGV。

本次新增的修复(关键)

改动作用
timeData: Object.freeze(parseTimeData(...))阻断 Vue3 对 timeData 内 5 个字段做深响应化,单次更新少 5 个 Proxy.set
timeRange 仅在首次/format 变更时赋值把每帧的"写 reactive 数组"省掉
毫秒模式频率 33ms → 100ms,秒模式 33ms → 200ms写频率降 3-6 倍,整体 reactive 写入量直接降到 ~原 10%
保留 _destroyed 守卫 + pause() 健壮化防御性保留,避免后续真销毁场景重现

如果还崩,下一步可以这样

如果按这个修改重新打包后,在毫秒 demo 上还能复现(可能性较小但存在),那基本可以确定是鸿蒙 V8 + Vue3 Proxy 的引擎层问题,到那时建议:

  1. 临时方案:把 demo 里 6 个 millisecond 实例改成 v-if 控制,只展示 1 个,或暂时屏蔽 millisecond demo;
  2. 彻底方案:在count-down.vue 中改造为 全局单一 ticker 派发(所有 count-down 实例共用一个 setInterval,在 raf 节奏内统一更新),把全局 setTimeout 数量从 N 个降到 1 个;
  3. 或者升级华为系统版本 / DevEco IDE。

请重新打包验证一下,看看新日志还会不会出现 SIGSEGV。

获取 TDesign 所有侧边栏链接,看看数量之类的

2026-06-21

js
// 1. 获取所有侧边栏链接,清洗文本
const sideLinks = [...document.querySelectorAll('.TDesign-doc-sidenav-link')]
  .map(item => item.innerText.replace(/\s|\n/g, ''));

// 2. 过滤:同时包含英文 + 中文的条目
const mixedTextList = sideLinks.filter(text => {
  const hasChinese = /[\u4e00-\u9fa5]/.test(text); // 是否有汉字
  const hasEn = /[a-zA-Z]/.test(text); // 是否有英文字母
  return hasChinese && hasEn;
});

console.log(mixedTextList);

uniappx 中 Icon 组件自定义前缀

2026-06-18

完整演进史(5 个阶段)

起点:抄 uniapp 版的 .t-icon-{name}::before { content } 模式

less
.t-icon-check::before { content: '\e001'; }
html
<view class="t-icon-check" />

uniapp 版好用,照搬到 uvue 应该也行——但马上撞墙。


阶段 ①:内置图标的"塌缩"问题

症状:内置图标渲染出来宽高都是 0 / 16px,size 设了 24px 也没用。

根因:uvue 的 view 不会自动从父级继承 font-size(与浏览器 / 微信小程序行为不同),子 view .t-icon-check 拿到的是默认 16px,而非父 .t-icon 的 24px;且 inline-flex 容器在 view 子元素上不会按字号撑开。

解决

  1. .t-icon 显式 width: 1em; height: 1em 撑开
  2. .t-icon-{name} 显式 font-size: inherit; width: 1em; height: 1em; display: flex
  3. 子节点也显式 font-family: 't'(不依赖继承)

📌 保留至今的代码:theme.less 里 each(@icons) 那段长注释就是这事的纪念碑。


阶段 ②:自定义图标完全无法显示(核心难题)

症状:用户写 prefix="my-icon" name="a-0" —— 期望渲染 .my-icon-a-0::before { content: '\e64d' }。但 demo 里 <style> 写的 .my-icon-a-0::before 完全不生效

根因:uvue 的 style isolation —— 子组件 t-icon 内的 <view class="my-icon-a-0"> 是子组件作用域,外部 demo 写的 .my-icon-a-0::before 选择器穿不进去。这跟原生小程序的 apply-shared 模式完全不同。

首次尝试 - 方案 A(external-class 思路):放弃伪元素,改让外部传 unicode 字符 + font-family 进来。

引入新 prop glyphChar

html
<text v-if="glyphChar" class="t-icon__glyph" :style="customStyle">{{ glyphChar }}</text>

把"图标怎么定义"的责任完全交给业务方:你给字符、你给字体族,组件只负责渲染。

📌 思路转折点:从"伪元素 + class 字典"模式 → "字符 + 字体族"模式。


阶段 ③:customStyle 同时挂在两个节点的丑陋设计

问题:上一步为了让 font-family 一定生效,把 customStyle 同时挂在 root view 和 inner text 上:

html
<view :style="rootStyle + customStyle">
  <text :style="customStyle">{{ glyphChar }}</text>
</view>

坏处:用户传 padding: 8px 会被应用两次,background 会画两次盒子。语义错乱

重构 - BEM 修饰符方案

引入 .t-icon--glyph 修饰符:

less
.t-icon { font-family: 't'; }
.t-icon--glyph { font-family: inherit; }   // ← 在 glyph 模式下重置
html
<view :class="rootClass" :style="rootStyle">  <!-- customStyle 只在这里 -->
  <text class="t-icon__glyph">{{ glyphChar }}</text>  <!-- 不挂任何 style -->
</view>

设计逻辑:

  • 重置.t-icon--glyph 把根上的 font-family: 't' 重置成 inherit,给 customStyle 让位
  • 注入:用户的 customStyle="font-family: 'my-icon'" 进入根节点
  • 继承:CSS 中 font-family 是天然继承属性,<text> 自动拿到 'my-icon'

customStyle 回归"只挂根节点"的 TDesign 通用约定。和上一阶段已存在的 .t-icon--image { font-family: inherit } 形成对称设计


阶段 ④:字体显示成"豆腐块"——隔离的第二层

症状:CSS 链路全打通,customStyle 也确实把 font-family: 'my-icon' 注入到了根,text 也确实继承到了。但屏幕上显示字符位置正确占位、字形是豆腐块

根因:uvue 的 style isolation 不只隔离选择器,@font-face 注册的字体 family 也只在该组件作用域可见

  • demo <style>@font-face { font-family: 'my-icon' } → 只有 demo 自己看得见
  • 子组件 t-icon 里的 <text> 是另一个作用域 → 拿到了 font-family: 'my-icon' 这个名字,但找不到对应字体资源 → fallback 到默认字体 → unicode \ue64d 在默认字体里没字形 → 豆腐块

解决:改用 uni.loadFontFace 全局注册字体:

ts
onMounted(() => {
  uni.loadFontFace({
    family: 'my-icon',
    source: "url(data:application/x-font-woff2;base64,...) format('woff2')",
    global: true,   // ← 关键:突破组件作用域
    success: () => { fontReady.value = true; },
  });
});

📌 这是 dcloud 官方为 uvue 字体隔离问题准备的逃生通道,是该问题的标准解法。


阶段 ⑤(你刚发现的):!important 是冗余的

观察:把 theme.less 里所有 !important 移除,行为完全正常。

为什么

选择器特异度来源
.t-icon(0,0,1,0)
.t-icon--glyph(0,0,1,0)类(同特异度,后写胜出
元素 inline style=(0,1,0,0,0)inline style,碾压一切 class

层叠顺序的自然规则已经保证了正确性,!important 是基于"防御 customStyle 写得不够强"的过度担心。移掉之后:

  • 代码更干净
  • 不再有 stylelint 注释噪音
  • 一旦未来有人需要在更深的组合场景里覆盖,也不会被 !important 卡住

最终架构(一图流)

三种渲染模式职责对照表

模式触发条件节点font-family 来源隔离突破方式
内置字体图标默认<view class="t-icon-{name}">.t-icon-{name} 显式 't'字体在组件内 @font-face 注册,无需突破
图片name 含 /<image>inherit(不需要)
自定义字符图标glyphChar 非空<text class="t-icon__glyph">根节点 customStyle 注入 + CSS 继承uni.loadFontFace({global:true}) 全局注册字体

核心结论

  1. uvue style 的隔离是"双层"的:选择器隔离(看得见也匹配不到子组件节点)+ @font-face family 隔离(family 名字跨不出作用域)。
  2. 跨组件传字体图标的优雅方案:组件提供 glyphChar prop(character-based 而非 class-based),由 customStyle 注入 font-family,BEM 修饰符 reset 默认字体。
  3. 跨组件注册字体的标准方案uni.loadFontFace({ global: true }),不要靠 <style> 里的 @font-face
  4. CSS 层叠规则已足够:inline style > class,同特异度后写胜出。!important 在这个场景没必要,是过度防御。

color-picker 预设色彩没有横向排列

2026-06-18

uniapp x 启动开发

2026-06-18

bash
# 一次性同步
sudo pnpm --filter="./packages/uniapp-x-components" run sync

# 监听同步
sudo pnpm --filter="./packages/uniapp-x-components" run sync:watch

uniappx 中的 button 的 icon

2026-06-18

uniappx 中的 button 的 icon,外层可以了,但是内层还是16

问题诊断

对照两张图:

图 1(外层 .t-icon:✅ 24 × 24,font-size 24px,width/height 1em,宽高对了 图 2(内层 .t-icon-search:❌ 16 × 16,但样式里明明写了 width: 1em; height: 1em

为什么 1em 在内层失效?看图 2 的样式——.t-icon-search没有 font-size 声明!那它的 1em 是按什么算的?

根本原因——uvue/uni-app 端 font-size 不继承

在 H5 浏览器里,font-size 是强继承属性,子元素 1em 会自动取父级的 24px。但在 uvue/uni-app 端:

  • view 的 font-size 不会从父 view 继承
  • 子节点没有显式 font-size 时,回退到全局默认(通常 16px)
  • 所以 .t-icon-search { width: 1em }1em = 16px(默认字号)→ 盒子 16×16

这就是为什么图 1 的 .t-icon 因为自己显式写了 font-size: @td-icon-size,1em 算出 24px;而 .t-icon-search 没写 font-size,1em 退化成 16px。

并且——伪元素 ::before 的字符渲染也跟着这个 16px 的字号走,所以图标真的就是 16 大小,跟外层 24×24 的盒子不匹配(图 1 中你能看到 24×24 盒子里塞了一个 16 的图标,正中央那块小放大镜就是这么来的)。

修复方案

在每个 .t-icon-{name} 字典展开里也补上 font-size: inherit(让它显式从父级 .t-icon 取 font-size)。这是 uvue 端的常见 workaround:显式 inherit 才能跨 view 继承

✅ 内层 16×16 问题已定位并修复

你的观察非常准确

对照两张图的关键差异

节点计算样式里有 font-size 吗?1em 算成多少盒子尺寸
外层 .t-iconfont-size: 24px(自己写的)24px24×24 ✅
内层 .t-icon-search没有16px(默认)16×16 ❌

图 2 你能直接看到 .t-icon-search 的样式面板里没有 font-size 那一行——这才是 1em 失效的真凶。

根因——uvue/uni-app 端的隐藏坑

浏览器原生:font-size 是强继承属性,子元素 1em 会自动取父级 24px ✅

uvue/uni-app 端:

  • view 的 font-size 不会自动从父 view 继承(与浏览器行为相悖的关键差异点)
  • 子节点没有显式 font-size 时 → 回退到全局默认 16px
  • 所以 .t-icon-search { width: 1em } → 用的是默认 16px → 盒子 16×16
  • 更糟的是,伪元素 ::before 渲染字符也用这个 16px → 图标真的就显示 16 大小

修复

字典展开里给每个 .t-icon-{name}font-size: inherit显式声明继承

less
each(@icons, {
  &-@{key} {
    font-family: 't' !important;
    font-size: inherit;   // 👈 关键:强制从父级 .t-icon 继承字号
    line-height: 1;
    width: 1em;            // 现在 1em = 父级 24px = 24px ✅
    height: 1em;
    ...
  }
});

验证后效果

  • 父级 .t-icon font-size: 24px → 子级继承 24px → 子盒子 24×24 ✅
  • 伪元素 ::before 字形按 24px 渲染 → 图标尺寸 24 ✅
  • 按钮 size 阶梯切换(small/medium/large)改 .t-icon font-size,子节点和伪元素全部联动

这条经验值得记下来

uvue/uni-app 样式坑:font-size 不会自动从父 view 继承到子 view。需要 1em 跨级生效时,必须在每一层显式 font-size: inherit。color 也有类似问题(图里 .t-icon 写了 color: rgb(255,255,255) 但子节点能否继承也得验证)。

uniapp x 中 button 的 icon

2026-06-18

为啥它 font-size 24px,但是看起来还是这么小,鼠标放上去还是 16

问题诊断

DevTools 给的两个关键信号:

  • 计算样式 font-size: 24px ✅ 生效了
  • 但 hover 提示 uni-view.t-icon 16 × 16 ❌ 元素盒子只有 16

font-size 是字号,不是盒子尺寸——它只决定字符渲染的字号,盒子大小是由内容撑开的。

.t-icon 模板里用的是 <view> 标签:

html
<view v-else :class="glyphClass" />  <!-- glyphClass = 't-icon-search' -->

关键差异(与 uniapp 端对照)

  • 浏览器原生 + <text> 标签:行内元素,::before content 字符按 font-size 自然撑开盒子
  • uni-app 端 <view> 标签:块级元素,但 uni-view 默认 width: 0::before 在 view 上的渲染不会主动撑开容器
  • 我父级又写了 display: inline-flex,inline-flex 收缩+子元素塌缩,最终 .t-icon 被压成 16×16(uni-view 的默认 minimum)

→ font-size 24 设置成功了,但盒子没跟着撑大,伪元素字符在 16×16 的盒子里被裁切,看起来还是 16。

修复方案

.t-icon 和字典展开的 .t-icon-{name} 都加上 width: 1em; height: 1em

less
.t-icon {
  display: inline-flex;
  width: 1em;          // 👈 关键:1em = 当前 font-size
  height: 1em;
  font-size: 24px;     // → 自动得到 24×24 的盒子
  ...

  each(@icons, {
    &-@{key} {
      width: 1em;       // 子字形节点也按字号撑开
      height: 1em;
      display: flex;
      align-items: center;
      justify-content: center;
      ...
    }
  });
}

为什么 1em 方案最优

  • 单一数据源:尺寸只由 font-size 决定,按钮 size 阶梯(18/18/20/24)→ icon 跟着自动 18×18 / 20×20 / 24×24,完全联动
  • 保持兼容:用户单独用 <t-icon size="32"> 时,inline style 改 font-size,盒子自动跟着变成 32×32
  • 不改模板:保留 <view> 标签,与 uvue 原生约束兼容
  • 行内布局inline-flex 让 icon 仍可与文字同行排列,不破坏按钮里 __icon + __content 的水平布局

uniappx 的 button 中的 icon 问题

2026-06-18

相比于 uniapp 版本的 button,uniappx 的 button 中的 icon 有点小,最右边那个也没左右居中

问题诊断

#现象根因
1图标按钮里的 icon 偏小(约 18-20px,应为 24px)demo 写死 <t-icon size="20"> 强制覆盖;同时 .t-icon 自身 font-size: 24px 拦截了外层 .t-button__icon 的级联,本来 large 应该走 24
2最右边放大镜 square 按钮 icon 偏左不居中less 给 &__icon { margin-right: 8px }无条件的;square 没 content 时这 8px 让 icon 整体右挪

改动清单

1. t-button.theme.less — 两处改动

less
// 改动 A:size mixin 中增加后代选择器,穿透 t-icon 自身 24px 默认值
.button-size(@size, @height, @paddingH, @fontSize, @iconSize) {
  .t-button--size-@{size} {
    // ...
    .t-button__icon { font-size: @iconSize; }
    .t-button__icon .t-icon { font-size: @iconSize; }  // 👈 新增
  }
}

// 改动 B:把 icon 间距从 __icon.margin-right 移到 __content.margin-left
&__icon  { /* 去掉 margin-right */ }
&__loading { /* 去掉 margin-right */ }
&__content { margin-left: 8px; }      // 仅在有 content 时生效

// square/circle 防御:用户硬塞 content 时也保持居中
&--square &__content,
&--circle &__content { margin-left: 0; }

2. [_example/icon-btn/index.uvue] (/Users/guowangyang/Documents/github/tdesign-miniprogram/packages/uniapp-x-components/src/components/t-button/_example/icon-btn/index.uvue) — 去硬编码

diff
- <t-icon name="app" size="20" color="#ffffff" />
+ <t-icon name="app" color="#ffffff" />

让按钮 size 阶梯(28/32/40/48)自动控制图标尺寸(18/18/20/24),不再每个 demo 手算

设计原则对齐

  • uniapp 版做法:.t-icon 不写 font-size,由外层(.t-button__icon { font-size: @icon-size })自然级联
  • uniapp-x 版约束:.t-icon 单独使用时也要有默认尺寸(24px),所以保留默认值 + 用 button 端后代选择器特异性(0,2,1 > 0,1,0)穿透。这样:
    • icon 单独用 → 默认 24px ✅
    • icon 在 button 里 → 跟随 size 阶梯 ✅
    • icon 显式传 size prop → inline style 优先级最高,仍能覆盖 ✅

快速甩飞 10 档或者连续情况下,都是延迟很明显,这里社区是怎么做的,是不是可以只管最后一步骤的

2026-06-17

你说的对,前面我搞复杂了。先冷静下来重新分析。

重新审视问题

社区主流做法(Vant / NutUI / iView / WeUI / 美团),我来一一对比:

做法
Vant Picker完全不做音效,靠系统原生的滚轮反馈(iOS 原生 picker-view)
NutUI Picker同上,不内置
uni-ui PickerView同上
iOS 原生 picker由系统 UIKit 在每次 cell 居中时由 SDK 触发 haptic + 系统声,不是 web 层做的
WeUI用小程序原生 <picker-view>,由微信内核处理
滴滴/美团等业务极少数自研需要 picker 反馈的,普遍只在最终落点响一声

关键事实没有任何一个 web 实现的滚轮组件,在快速滑动时能做到逐档实时音效。原因就是你现在遇到的问题:

  1. 小程序 setData 机制:每次 this.offset = newOffset 在小程序里要经过 setData → 视图层 → 渲染,本身有 16ms+ 延迟
  2. InnerAudioContext 起播延迟:微信小程序首次播放 100~300ms,后续即便复用实例也有 30~80ms 抖动
  3. uni.vibrateShort 节流:小程序底层强制 30~50ms 防抖
  4. CSS transform 动画期间无 JS 钩子:惯性段我们用 setTimeout 模拟节奏,但 setTimeout 在小程序逻辑层和视图层渲染线程不同,对不齐
  5. 音频/振动 API 本身耗时:在低端安卓机,单次 play() 调用进 webview-bridge 就要 50ms+

我前面的方案——onTouchMove 跨档检测 + 惯性段缓动 setTimeout 预排——在原理上就是对抗系统物理限制,做得再好也是"用算法逼近",永远做不到真正同步。

你说的"只管最后一步骤"才是真正的工程解

这才是社区的真实做法。仔细想:

方案工程复杂度体验卡顿风险
逐档实时(当前)看似 iOS,实测延迟
只在最终落点响极低干净利落
落点响 + 强振动极低触感像 iOS

只在最终落点

  • ✅ 代码极简,没有 setTimeout 池、没有抽样、没有惯性预测
  • ✅ 不会跟系统抢音频/振动资源
  • ✅ 用户感知是"我选完了——咔哒,确认",跟系统 picker 行为一致
  • ✅ 振动可以用 type: 'medium' 加重,弥补只响一声的"轻量感"

实际上原生 iOS picker 的体验本质上也是:滚动过程中是 SDK 内部的 audio engine 实时跟踪 cell 居中(人体感觉非常快是因为它根本没经过 JS 层),如果你真的实测,你以为的"每档都响"其实是大脑补全的视觉关联——你在快速滑动时根本听不出是 1 声还是 5 声,停下后那一声才是核心反馈。

建议:回退到"只管最后一步"

具体改法(比现在简单 90%):

  1. picker-item.vue:删掉 _lastTickIndex_inertiaTickTimersonTouchMove 中的跨档检测、onTouchEnd 中的缓动 setTimeout 预排。只保留 updateSelected(index, true) → 调用 triggerColumnChange(其实代码本来就有,原地不动)

  2. picker.vue:把 playTickFeedback 调用从 triggerColumnTick 移到 triggerColumnChange。删除抽样逻辑。

  3. triggerColumnTick / pick-tick 事件保留还是删? 我建议保留作为业务高级钩子(万一未来有人确实想自己做逐档逻辑),但组件内部不再调用 playTickFeedback

结果

  • 滑一档 → 落定 → 1 声"哒" + 1 次振动
  • 快速甩 10 档 → 滚动过程不响(更接近 iOS 系统 picker)→ 停下 → 1 声"哒" + 1 次振动
  • 用户感知:干净利落、毫无延迟

想问你三个决策点

在动手前先对齐:

问题 1:是否完全采纳"只管最后一步"?

  • A. 是,全面回退(推荐 ✅)
  • B. 还想保留逐档音效作为可选模式(加个 tickStrategy: 'final' | 'inertia' | 'realtime'

问题 2pick-tick 事件保留吗?

  • A. 保留,但组件内部不调用(业务想自己做时用)
  • B. 直接删掉,连同 triggerColumnTickenable-tick-* 属性等都简化

问题 3:振动强度

  • A. type: 'medium'(更明显的"咔",弥补只响一声)
  • B. type: 'light'(保持现在的轻盈)

我的推荐组合是 1A + 2A + 3A:完全回退到落点响一声 + 保留 pick-tick 钩子兜底 + 振动加重。

你拍板,我立刻动手。

逐档触发

2026-06-17

✅ 改动汇总

#文件变更
1picker-item.vuecreated/onTouchStart 维护 _lastTickIndexonTouchMove 跨档时调用 triggerColumnTickonTouchEnd 同步该索引
2picker.vue新增 triggerColumnTick 方法 → $emit('pick-tick', { column, index })
3picker/props.ts新增 onPickTick 声明
4picker/type.ts新增 onPickTick 类型
5picker/README.md文档新增 pick-tick
6tdesign-api/db/TDesign.db插入一条 Event 记录(id=1781676506),避免下次脚本回写覆盖

📱 业务侧使用示例

html
<t-picker @pick-tick="onPickTick" @change="onChange">
  <t-picker-item :options="years" />
</t-picker>
js
// 预创建多个 audio 实例轮询播放,避免快速滑动时音效跟不上
const audioPool = Array.from({ length: 4 }, () => {
  // #ifdef MP-WEIXIN
  const a = wx.createInnerAudioContext();
  a.src = '/static/picker-tick.mp3'; // 30ms 短音效
  return a;
  // #endif
});
let audioIdx = 0;

methods: {
  onPickTick({ column, index }) {
    // 音效(轮询,避免上一声未播完)
    const audio = audioPool[audioIdx];
    audioIdx = (audioIdx + 1) % audioPool.length;
    audio?.play();

    // 触觉反馈
    // #ifdef MP-WEIXIN || APP-PLUS
    uni.vibrateShort({ type: 'light' });
    // #endif
  },
}

⚠️ 注意事项

  1. 触发频率pick-ticktouchmove 中检测到跨档时立刻触发,正常匀速滑动每越过一档触发一次;惯性飞滑时 touchend 后用的是 transform 动画,惯性段不会逐档触发(因为没有 touchmove 事件)。如需"惯性飞滑也哒哒哒响",需要在 onTouchEnd 里另起一个定时器按预测落点节奏播音效,复杂度高,本次未做。
  2. 音频实例池:用单个 audio 实例快速 .play() 在 H5/小程序上会出现"播不全/丢音",所以推荐 4 个实例轮播。
  3. RELATION_MAP 调用:使用了 ?.triggerColumnTick?.()(可选链),即使父 picker 是旧版本不存在该方法也不会报错,向后兼容。

🔄 别忘了

  • 原生小程序版 packages/components/pickerpicker-item 还没改(你的需求只针对 uniapp 版还是要两个版本同步?)。
  • 上面 SQLite 我只插了 UniApp(platform_framework=128)。如果要小程序版也支持,需把掩码改成 192(128+64)或单独再插一条 64 的,并改原生小程序版的 .ts 实现。

Picker Tick

2026-06-17

✅ 方案 B+ 实施完成

改动汇总

#文件变更
1picker/tick-sound.ts新建,导出 30ms 内置"咔哒"声 base64 data URI(1824 字符 ≈ 1.4KB)
2picker/props.ts新增 3 个 props(按字典序插入)
3picker/type.ts新增 3 个属性类型
4picker/picker.vue引入音效;triggerColumnTick 内置 playTickFeedback;4 实例懒加载音频池轮询播放;振动 30ms 节流;beforeUnmount 销毁释放;emitspick-tick
5picker/README.mdProps 表新增 3 行
6tdesign-api/db/TDesign.db插入 3 条 Props(id=600/601/602,platform_framework=128)

业务侧使用(一行属性搞定 🎉)

html
<!-- H5 / App / 大部分非微信小程序:开箱即用 -->
<t-picker enable-tick-sound enable-tick-vibrate>
  <t-picker-item :options="years" />
</t-picker>

<!-- 微信小程序:因 audio 不支持 data URI,需自带短音效文件 -->
<t-picker
  enable-tick-sound
  enable-tick-vibrate
  tick-sound-src="/static/tick.mp3"
>
  <t-picker-item :options="years" />
</t-picker>

关键技术细节

设计决策说明
默认 false不影响存量用户行为,必须显式开启
音频池 4 个实例轮询快速滑动时单实例 play() 会丢音,4 个实例足以覆盖最快滑动节奏
懒加载首次需要播放时才创建 InnerAudioContext,未启用属性时零开销
beforeUnmount 销毁调用 audio.destroy() 释放原生音频资源,防止内存泄漏
振动节流 30ms防止快速滑动时连续触发 vibrateShort 导致设备无响应
平台兼容静默降级typeof uni !== 'undefined' && uni.vibrateShort/createInnerAudioContext,不支持的平台静默跳过
obeyMuteSwitch=true跟随系统静音开关,符合 iOS 用户预期
保留 pick-tick 事件业务仍可监听该事件做额外定制(埋点、自定义音效等)

⚠️ 已知限制

  1. 微信小程序的 data URI 限制:微信 wx.createInnerAudioContext().src 不支持 data:audio/wav;base64,...必须tick-sound-src 静态资源路径才能听到声音;振动则不受影响。
  2. 惯性飞滑无音效pick-tick 来自 touchmove 检测跨档,惯性段是 transform 动画,没有 touchmove 事件,所以惯性飞滑期间不响——这点与 iOS 原生略有差异。如需做到 iOS 那种惯性段也响,需要在 onTouchEnd 里另起预测定时器,复杂度高,本次没做。
  3. 原生小程序版本未同步packages/components/picker 还是老的,要不要我也加上?

要不要我把原生小程序版(packages/components/picker)也加上同样的能力?

Picker 滑动多个时候有延迟

2026-06-17

这是个好问题。先把"滑动多个时候有延迟"拆开看是哪种延迟,再对症下药。我先看一下当前 picker-item 中的 touchmove 实现,确认现状:

现在我把"延迟"的根因分析清楚。

你说的"滑动多个时候有延迟",到底是哪种延迟?

通常会有 4 种感受是用户口中的"延迟",且本方案有 3 种是真实存在的

🐢 延迟 1:音效本身播放延迟(小概率)

uni.createInnerAudioContext().play() 在小程序上首次调用大概有 50~200ms 的解码 + 起播延迟。后续调用通常 < 30ms。

  • 排查:纯振动、不开音效,看是不是还卡 → 如果只有开了音效才卡,问题在音频
  • 这个延迟与"滑动多档"无关

⚠️ 延迟 2:惯性滑动期间没有 tick最可能是这个

onTouchEnd:手指松开后,offset 一次性赋值为 finalOffset,然后用 CSS transform 动画过渡。整个惯性段是 CSS 动画在跑,没有 touchmove 事件,所以期间 _lastTickIndex 不会更新,音效也不会触发

用户感受:

  • 慢慢拖:每跨一档"哒",正常
  • 快速甩一下:手指松开瞬间响一声(最后那次 touchmove),然后整个惯性飞过 5~10 个选项过程中完全没声音,到最后停下来才靠 triggerColumnChange 响一次。给人感觉音效跟滚动严重脱节

这就是你说的"滑动多个时候有延迟"——视觉滚动了 5 档,音效只响了 1~2 次,听感像延迟。

🐢 延迟 3:触发频率过快导致音频实例被打断

快速滑动时 onTouchMove 60fps 触发,跨档可能间隔 16~30ms。当前音频池只有 4 个实例,且每次都先 audio.stop() + audio.seek(0) + audio.play()stop 之后立刻 play 在某些平台是异步的,会出现"前一声还没出来就被 stop 掉"导致丢音。

🐢 延迟 4:setData 节流引起的视觉延迟

每次 onTouchMove 都赋值 this.offset = newOffset(uniapp 编译到小程序底层是 setData),高频 setData 会导致 UI 卡顿。但这个不是音效引入的,而是组件原本就有


三个真问题的修复方案

修复 1:惯性段也要有 tick(最关键)

onTouchEnd 里既然知道了 offset → finalOffset 的距离 + 动画时长 animationDuration,我们可以预测惯性段会跨越的档位数,setTimeout 按节奏发出 tick

js
// onTouchEnd 中已有:finalOffset、index、scrollItems、animationDuration
// 计算惯性段需要发的 tick 次数和节奏
const fromIndex = this._lastTickIndex; // 手指离开瞬间的档位
const toIndex = index;                  // 惯性最终落点档位
const tickCount = Math.abs(toIndex - fromIndex);

if (tickCount > 0 && animationDuration > 0) {
  // 用缓动函数让 tick 节奏跟视觉一致:开始密、末尾疏(cubic-bezier 减速)
  const direction = toIndex > fromIndex ? 1 : -1;
  const parent = this[RELATION_MAP.PickerItem];
  // 清理旧定时器
  if (this._inertiaTickTimers) {
    this._inertiaTickTimers.forEach(t => clearTimeout(t));
  }
  this._inertiaTickTimers = [];
  for (let k = 1; k <= tickCount; k++) {
    // 缓动:t^0.6 让前面密集后面稀疏,模拟减速感
    const progress = Math.pow(k / tickCount, 0.6);
    const delay = progress * animationDuration;
    const tickAt = fromIndex + direction * k;
    const timer = setTimeout(() => {
      parent?.triggerColumnTick?.({ index: tickAt, column: this.columnIndex });
    }, delay);
    this._inertiaTickTimers.push(timer);
  }
}
this._lastTickIndex = index;

修复 2:音效池扩容 + 不要 stop()

不调 stop(),多数平台 play() 会自动重置;只增大池容量:

js
const TICK_AUDIO_POOL_SIZE = 8; // 从 4 扩到 8

// playTickFeedback 中:
try {
  if (typeof audio.seek === 'function') audio.seek(0);
  // 移除 audio.stop() —— 它在小程序异步且会造成下一次 play 丢音
  audio.play && audio.play();
} catch (_) { /* noop */ }

修复 3:快速滑动时降级"每 N 档响一次"

如果连续 1 秒内触发了超过 20 次 tick,可以做"音效抽样"——每 2 档响一次,听感更像 iOS 系统:

js
// playTickFeedback 入口加入抽样
const now = Date.now();
this._tickTimes = (this._tickTimes || []).filter(t => now - t < 200);
this._tickTimes.push(now);
// 200ms 内超过 6 次 tick 就抽样:每 2 档响一次
const isTooFast = this._tickTimes.length > 6;
if (isTooFast && (this._tickSampleCount = (this._tickSampleCount || 0) + 1) % 2 !== 0) {
  return;
}

我的建议

先只做修复 1(惯性段补 tick),这是 90% 用户感知到"延迟/不同步"的根本原因。修复 2 和 3 是优化项,做完再看效果决定要不要加。

要我直接动手做修复 1 吗?需要改的文件:

  • picker-item.vue onTouchEnd 里补惯性段 tick + beforeUnmount 清理定时器

或者你想要"豪华版",三个修复一起上?

[tabbar] 为什么 normal 下已经有安全区了呢,这个跟 round 不一样吗

2026-06-17

好问题!这正是我之前没解释清楚的关键点。我从 CSS 渲染机制角度详细说一下两者的本质差异:

看代码本质区别

来自 tab-bar.less

less
&--normal&--safe {
  padding-bottom: env(safe-area-inset-bottom);   // ① 用 padding
}

&--fixed&--round&--safe {
  bottom: env(safe-area-inset-bottom);           // ② 用 bottom 偏移
}

两种形态用了完全不同的实现策略——这就是关键。

视觉对比

              normal 形态                          round 形态
        ┌─────────────────────┐               ┌─────────────────────┐
        │                     │               │                     │
        │     页面内容         │               │     页面内容         │
        │                     │               │                     │
        ├─────────────────────┤               │                     │
        │ [图][图][图][图]      │ ← 内容区      │   ┌──────────────┐  │
        │                     │               │   │[图][图][图][图]│  │ ← bar 实体
        │  (padding-bottom)   │ ← 安全区       │   └──────────────┘  │
        ├─────────────────────┤ ← 屏幕底沿     │                     │ ← 上抬空白
        └─────────────────────┘               └─────────────────────┘ ← 屏幕底沿

normal:bar 是一整块矩形,内部留 padding   round:bar 是个胶囊,整体被推上去
       高度 = 内容 + padding(安全区)               高度 = 内容(不含上抬量)

为什么 normal "看起来已经有安全区"?

因为 padding 是元素自身的一部分

维度normal(用 padding-bottom)round(用 bottom)
元素视觉高度包含安全区 ✅不含安全区 ❌
元素盒模型高度(boundingClientRect.height)包含安全区 ✅不含安全区 ❌
安全区在哪在元素内部(padding)在元素外部(被 bottom 顶上去的空白)

核心 CSS 原理

  • padding 撑大盒子本身 → bar 元素的真实高度变高 → getBoundingClientRect().height 自然就含了
  • bottom 只是定位偏移 → bar 还是原来那么高,只是位置往上挪 → getBoundingClientRect().height 不变

那 round 为什么不直接用 padding?

因为 round 形态是圆角胶囊+悬浮阴影的设计:

less
&--round {
  margin-left: 32rpx;
  margin-right: 32rpx;
  border-radius: 999px;  // 完整胶囊圆角
  box-shadow: @tab-bar-round-shadow;
}

如果 round 用 padding-bottom: env(...),会导致:

  1. 胶囊变成上窄下宽的奇怪形状(padding 撑高内容区)
  2. 圆角失效(border-radius: 999px 在矩形上才是完美胶囊)
  3. 阴影错位

所以 round 必须用 bottom 偏移——让胶囊保持原本形状,整体向上移开 home indicator

那为什么 issue 里 normal 形态也报 bug?

理论上 normal 形态 getRect.height 已含安全区,不应该有 bug

但实际有几种情况会让 normal 形态也翻车:

  1. 测量时机过早mountednextTickgetBoundingClientRect,部分 iOS 真机此时 env(safe-area-inset-bottom) 还没解析完,padding 还是 0,测出来的 height 不含安全区
  2. iOS WebView 在某些版本返回的 boundingClientRect 不包含 env() 计算出的 padding(已知 bug,社区有反馈)
  3. uni-app 编译到某些平台时 env() 支持不完整

所以保守做法(你现在的版本):只要 safeAreaInsetBottom=true,无脑补一份

取舍权衡

你目前简化的 if (this.safeAreaInsetBottom) height += getSafeAreaBottom() 方案:

形态修复前当前修复严格修复(按 shape 区分)
normal(理想环境)✅ 正好⚠️ 可能多 34px✅ 正好
normal(iOS 测量异常)❌ 缺 34px✅ 正好❌ 缺 34px
round❌ 缺 34px✅ 正好✅ 正好

简化方案的"代价"是:理想环境的 normal 形态可能在底部多 34px 空白。但如果 issue 里的实际场景就是 normal 形态报 bug,说明真机环境就是不理想的,简化方案反而更稳。

建议

可以这样做最终决策(看你倾向):

A. 保持现状(简化方案):覆盖所有异常场景,代价是理想环境可能多空白 B. 折中:先 getRect,如果测出的 height 明显小于"内容估算高度 + 安全区",才补——能区分"测量翻车"和"测量正常"两种情况

如果想要更精准,我可以帮你改成 B 方案(用一个阈值或对比逻辑判断是否需要补)。

看看 faq 的其他部分,有没有可以优化的语句,不专业的语句可以优化的

2026-06-16

好的,我已经读完整份 faq.md(在你提供的 attached 内容里)。整体写得不算差,但作为对外官方文档,口语化、指代不清、措辞过随意的地方有一些。下面分两块:

  1. 我的诊断(哪里不专业 / 不够清晰)
  2. 给出可直接落地的修订版

一、问题诊断

位置原文问题
写在前面"可以查看这篇文章""这篇文章"过于口语化,建议明确"实现原理介绍"
外部样式类"在 tdesign-uniapp 中被转成了 props""转成了"偏口语;"被" 字句不必要
外部样式类"使用的时候要在样式文件中增加 :deep 标记,否则会因为 scoped 而失败""增加 :deep 标记"不准确,:deep()伪类选择器,不是"标记";"会因为 scoped 而失败"指代不明,应说"会因 scoped 隔离导致样式不生效"
外部样式类"如果是非页面级别组件,需要增加 options: { styleIsolation: 'shared' }"这是小程序端特有,且只对编译到 mp-weixin 等平台生效;H5/App 不需要。原文没区分平台,会误导
事件参数"都被去掉了 detail 一层""去掉了 detail 一层"口语化;"detail 一层"指代不清(一层什么?)
事件参数"这样做是为了简化使用。tdesign-uniapp 中很多组件都采用了这种方式。"后半句多余且不严谨——不是"很多",是"所有";建议合并精简
onPageScroll"在小程序、APP 等平台下,需要业务自己在页面中监听 pageScroll 事件,这是因为动态的监听不生效""业务自己" → "业务方";"动态的监听不生效"语病——应为"动态注册的页面生命周期不生效"
onPageScroll"这里给出一个最佳实践之一"病句:"一个 ... 之一"重复
onPageScroll 注释"// 该方法内部会通过 event-bus,传递参数给对应的组件""传递参数给对应的组件"→"将滚动事件分发到组件内部"更准
slot 类型提示"uniapp 给的脚手架工程配置有问题""给的" 口语;"工程"→"模板"
slot 类型提示"另外,它这个脚手架太老了,自己的 tsconfig.json 还飘红,升级下 @vue/tsconfig 可解决。""它这个" / "太老了" / "飘红" / "升级下" 全是口语,最不专业的一段
slot 类型提示"这里有一个开箱即用的 uniapp Vue3 [脚手架项目]...你可以打开看看。""你可以打开看看"非常口语
visible 受控标题与正文写得没问题,但列表里 drawer 在 tdesign-uniapp 实际不存在(需确认,下面会查证)

我顺便核实一下 drawer 是否真的存在,避免我在修文档时把不存在的组件保留下来:

好,drawercolor-picker 都存在,原列表无需删除。

那我直接落地修订。下面是「先给你看修订对照,再问你是否落盘」的方式——但你这次问题是"看看有没有可以优化的语句,不专业的语句可以优化的",更倾向于建议而不是直接改。所以我不动文件,先把建议改写贴出来,等你确认后再修改

二、修订建议(按段落给出)

① "写在前面"

  • 原:TDesign UniApp 的实现原理,可以查看这篇文章。
  • 改:TDesign UniApp 的实现原理,可参考原理介绍。

② "外部样式类"

  • 原:

    tdesign-miniprogram 中的 externalClasses,在 tdesign-uniapp 中被转成了 props

    传参方式不变,都是 t-class="xxx-class"。使用的时候要在样式文件中增加 :deep 标记,否则会因为 scoped 而失败,如果是非页面级别组件,需要增加 options: { styleIsolation: 'shared' }

  • 改:

    tdesign-miniprogram 中的 externalClassestdesign-uniapp 中以 props 形式提供。

    传参方式保持一致,仍为 t-class="xxx-class"。使用时需注意:

    • 在样式文件中通过 :deep() 选择器穿透,否则会因 <style scoped> 隔离导致样式不生效;
    • 在小程序平台下,若组件并非页面级组件,需要额外声明 options: { styleIsolation: 'shared' } 以解除组件样式隔离。

③ "事件参数"

  • 原最后一句:"这样做是为了简化使用。tdesign-uniapp 中很多组件都采用了这种方式。"
  • 改:"此约定适用于 tdesign-uniapp 全部组件,目的是简化使用。"
  • 同时把上文 "都被去掉了 detail 一层" 改为 "统一去除了 e.detail 包装层"。

④ "onPageScroll"

  • 原:

    在小程序、APP 等平台下,需要业务自己在页面中监听 pageScroll 事件,这是因为动态的监听不生效。这里给出一个最佳实践之一。

  • 改:

    在小程序与 App 平台下,组件内部动态注册的页面生命周期钩子不会生效,因此需要业务方在页面中显式监听 onPageScroll 并转发给组件库,参考用法如下:

  • 注释 // 该方法内部会通过 event-bus,传递参数给对应的组件// 该方法内部通过 event-bus 将滚动事件分发到组件内部

⑤ "slot 类型提示"(这段口语最重,重点重写

  • 原:

    uniapp 给的脚手架工程配置有问题,src/env.d.ts 文件的 vue 声明不对,没有声明 slot 的类型。

    解决方案:

    1. 注释掉 src/env.d.ts 文件中 vue 的声明
    2. tsconfig.json 中配置 "moduleResolution": "bundler"

    ...

    另外,它这个脚手架太老了,自己的 tsconfig.json 还飘红,升级下 @vue/tsconfig 可解决。

    这里有一个开箱即用的 uniapp Vue3 脚手架项目,支持自动导入、类型提示等,你可以打开看看。

  • 改:

    uniapp 官方脚手架的默认配置中,src/env.d.tsvue 模块的类型声明不完整,未导出 slot 相关类型,会导致使用组件时缺失类型提示。

    解决方案:

    1. 注释掉 src/env.d.tsdeclare module 'vue' 部分;
    2. tsconfig.json 中将 compilerOptions.moduleResolution 设置为 "bundler"

    ...

    此外,官方脚手架自带的 tsconfig.json 在新版 TypeScript 下会出现类型报错,可通过升级 @vue/tsconfig 至最新版本解决。

    推荐使用我们提供的开箱即用模板:tdesign-uniapp-starter,已内置自动导入、类型提示等配置。

⑥ "visible 受控"(基本可保留,仅微调)

  • 原:"下面几个组件在关闭时,需要父组件中设置 visiblefalse,否则无法再次开启。也就是 visible 只能是受控的。可以用 v-model:visible 语法糖,可参考对应组件示例。"
  • 改:"以下组件在关闭时,需要父组件主动将 visible 重置为 false,否则无法再次打开。即这些组件的 visible 必须以受控方式使用,推荐使用 v-model:visible 语法糖,具体可参考各组件示例。"

⑦ "v-model 写法"(上一轮我刚加的,无需大改)

仅一处建议:把 "(与 React 版、原生小程序版保持一致)" 移到句末并括号化,读起来更顺:

tdesign-uniapp 所有受控组件统一使用 value 作为双向绑定属性名,而非 Vue 3 默认的 modelValue(与 React 版、原生小程序版保持一致)。


我的处理方式

按你之前的偏好(先征询、不擅自改用户代码),我没有动文件,先把上面 7 处建议交给你确认。

如果你 OK,我可以:

  • 全部按上面文案落盘;
  • 或你只勾选其中几处(比如只改最口语的 ⑤ slot 类型提示和 ④ onPageScroll),我就只改那几处。

需要我直接全部应用到 faq.md 吗?

非微信小程序下 getRelationNodes 不存在,这个能否优化下

2026-06-15

问题分析

swiper.vueupdateNav 方法目前调用了 this.getRelationNodes('./swiper-nav')

js
updateNav(currentValue) {
  if (this.navigation) return;
  const $nav = this.getRelationNodes('./swiper-nav')?.[0];
  if (!$nav) return;
  ...
}

getRelationNodes微信小程序原生 Component 的方法,在 uniapp 体系下(H5、APP、其他小程序)并不存在。原生小程序版本里靠 relations 配置 + getRelationNodes 来通信,但 uniapp 版本里通过 ParentMixin / ChildrenMixin 实现的 provide/inject 关系机制,子节点会被收集到 this.children 上(参见 relation.jsbindRelation)。

而且这里其实逻辑本身有问题

  1. 原小程序 updateNav 的目的是把 current/total/direction/paginationPosition 同步给 <t-swiper-nav> 子组件。
  2. uniapp 版本里 <t-swiper-nav> 已通过 props 双向绑定 (:current="navCurrent":total="list.length":direction:pagination-position),本身就是响应式的,根本不需要再手动 setData。
  3. 当前的 updateNav 写法 this.current = currentValue; this.total = list.length; 是直接对 props 赋值,会触发 Vue 的 props 修改告警(且不会生效),而 direction/paginationPosition 自赋值也无意义。

方案

根据 uniapp 版本的实际情况,最干净的做法是直接移除 updateNav 的 DOM 查询逻辑。因为:

  • 模板上 <t-swiper-nav> 通过 props 已实现响应式同步;
  • watch.navCurrent 实际上不需要再做任何事(保留 watch 也行,但里面什么都不用做);
  • 既然 watch 内部什么都不用做,就把 watch 也移除,逻辑更清晰。

我来直接修改:

修改完成。下面验证一下结果:

修改已确认生效。下面对本次优化做个说明:

优化说明

问题

swiper.vue 里的 updateNav 方法用了 this.getRelationNodes('./swiper-nav'),这是微信小程序原生 Component 独有的 API,在 H5、APP、其他小程序端都不存在,会报错。

不能直接套用 this.children

理论上 uniapp 体系下 ParentMixin 通过 provide/inject 机制把子节点收集到 this.children,可以拿到 <t-swiper-nav> 实例。但实际上根本不需要

真正的根因 —— 这段代码本身是冗余的

原小程序版 updateNav$nav.setData() 是因为原生小程序 relations 是组件实例间的弱关联,子组件无法响应式拿到父组件 props 变化,所以才需要手动 setData 同步。

而在 swiper.vue 模板里,<t-swiper-nav> 已经通过 props 完成了响应式绑定:

html
<t-swiper-nav
  :current="navCurrent || 0"
  :total="list.length || 0"
  :direction="direction || 'horizontal'"
  :pagination-position="paginationPosition || 'bottom'"
  ...
/>

navCurrent 一变,子组件 current 自动更新,根本不需要再去查找子节点手动赋值。

并且原 updateNav 内的 this.current = currentValue this.total = list.length 实际上是在直接修改 props,会触发 Vue 的"Avoid mutating a prop directly"告警,本身就是一段错误代码。

处理方式

直接移除 updateNav 方法和 watch.navCurrent

  • 模板已通过 props 实现响应式同步;
  • watch 中只是调用 updateNav,移除后 watch 体内为空,所以一并删除;
  • mounted 里的 this.navCurrent = this.current 保留,作为初始化。

逻辑更简洁,且彻底规避非微信小程序端调用不存在的 getRelationNodes 的问题。

2026H1 紧凑版本

2026-06-11

负责 TDesign 移动端组件开发与多端适配,横跨 miniprogram、mobile-vue、mobile-react 三大仓库。在小程序侧,主导 tdesign-uniapp 从 0.1.0 到 0.9.0 的全生命周期建设,实现 Vue2/Vue3 + H5 + 微信小程序多端适配,新增 Table、AI Chat 等组件并完成主题系统搭建;在 Vue 侧,增强 Table(fixedRows、scroll-to-bottom)、Dialog 异步关闭、Message 单例/间距、Slider 垂直模式等能力,完成 3 个版本发布;在 React 侧,新增 DateTimePicker 组件,扩展 Table/Calendar/Tabs/Popup 等组件能力,并系统性补充多组件测试用例。同时发表 TDesign UniApp 相关 KM 文章 2 篇,沉淀跨端适配方案与实践经验;TDesign UniApp 项目获得腾讯微创新奖。过去一年累计提交 109 次,净增代码 15.8 万行,完成 8+ 版本发布,覆盖新组件开发、Bug 修复、测试补充与国际化文档,对 TDesign 移动端三端生态的完整性和质量提升起到核心推动作用。

2026H1 原始数据

2026-06-11

tdesign-miniprogram

  1. UniApp 从无到有,全平台适配 — 主导 tdesign-uniapp 从 0.1.0 → 0.9.0 的 多个 个版本发布,支持 Vue2/Vue3 + H5 + 微信小程序等多端
  2. Table 组件开发 — 新增 Table 组件
  3. 主题系统 — 实现 theme-light 支持 及 uniapp 主题样式
  4. Chat 组件库 — 发布 tdesign-uniapp-chat 0.1.0 ~ 0.2.3,包含 chat-list、chat-thinking 等组件
  5. 多项 Bug 修复 — stepper、upload、search、dialog、sidebar 等组件问题修复

tdesign-mobile-vue

  1. Table 组件增强(scroll-to-bottom、footerSummary、fixedRows)
  2. Dialog 异步 onConfirm 支持 (#2166)
  3. Message 组件 single/gap 属性 (#1756)
  4. Slider 垂直模式 (#1745)
  5. Form string pattern 支持 (#1972)
  6. 修复 cascader/tabs/radio/pull-down-refresh 等多组件问题
  7. 发布 v1.9.1、v1.10.1、v1.11.0-beta 共 3 个版本

tdesign-mobile-react

  1. DateTimePicker 新组件 (#672)
  2. Table 组件 fixedRows + column.fixed 支持 (#662)
  3. Calendar switchMode 支持 (#663)
  4. Tabs 自动滚动到激活 tab (#693)
  5. Popup duration 属性 (#691)
  6. 修复 sticky/pull-down-refresh/guide/notice-bar 等组件问题
  7. 补充 cascader/tag/toast/back-top/footer 等组件测试用例

TD uniapp

2026-05-19

Vue3 hx 需要dist

榜首

2026-05-19

tdesign-uniapp getting-started 改动

2026-04-09

文档结构变化

修改前(扁平、碎片化):

## 使用
  1. 引入样式文件(CLI / HBuilderX)
  2. 在文件中使用(手动导入示例)
## 自动导入(推荐)     ← 与"使用"平级,逻辑不合理
  CLI 模式
  UNI_MODULES 模式      ← 命名不统一
## 编辑器提示            ← 独立章节,不清楚是否必须

修改后(步骤式、层级清晰):

## 使用
  ### 步骤一:引入样式文件
    #### CLI 模式
    #### HBuilderX 模式
  ### 步骤二:注册组件
    #### 自动导入(推荐)    ← 归入使用流程
      CLI 模式 / HBuilderX 模式(统一命名)
    #### 手动导入
  ### 步骤三:配置编辑器提示(可选)

具体改进点

#改进说明
1步骤编号添加「步骤一/二/三」,用户按顺序操作一目了然
2自动导入归入使用流程从独立二级标题降为「注册组件」的子步骤,逻辑更合理
3手动导入独立小节原来混在步骤 2 中,现在与自动导入并列,对比清晰
4自动导入增加使用示例配置完 easycom 后,补充了直接使用组件的代码示例
5统一模式命名"UNI_MODULES 模式" → "HBuilderX 模式",全文一致
6编辑器提示标注可选加上"(可选)",避免用户误以为是必须步骤
7精简编辑器提示描述去掉冗余的"安装注册 TDesign 之后",直接说明操作

为什么不同写法的 uniComponent 会导致类型提示不一样

2026-03-31

原理:

uniComponent 是一个纯 JS 函数(定义在 .js 文件中,没有 TypeScript 类型注解),返回类型被推断为 any

当 export default uniComponent(...) 时,Vue 的类型系统拿到的 default export 就是 any

当 export default { ...uniComponent(...) } 时,虽然展开的内容也是 any,但外层是一个对象字面量,Vue/TypeScript 能将其推断为一个合法的 Vue 组件选项对象(ComponentOptions),从而提供具体类型

TS 引入 JS 类型报错

2026-03-31

TS 文件引入 JS 文件,会报错 xx implicitly has an 'any' type。也就是所有的TS必须引入TS,或者有dts的JS。

【import { IsURLOptions } from …

2026-03-31

【import { IsURLOptions } from 'validator/es/lib/isURL'】

改成了

【import { IsURLOptions } from '@common'】

@brand-color 和 @brand-color-…

2026-03-11

@brand-color 和 @brand-color-7 在浅色模式下效果是一样的,因为它们的值是一样:

https://github.com/Tencent/tdesign-common/blob/develop/style/web/theme/_light.less#L69

但在暗色模式下却不是相等的:

https://github.com/Tencent/tdesign-common/blob/develop/style/web/theme/_dark.less#L68

为了在浅色和暗色模式下都达到最好的展示效果,两种模式下会分别取浅色/暗色两套色板不同色阶的位置颜色,但语义化的 Token 含义是稳定的,就是 @brand-color 它表示了整个组件库的主色调。当你在组件样式中使用了 @brand-color 时,就可以在不同默认切换时自动使用最合适的色板值。

基础组件库短期内无法被AI取代。业务需要全局设计风格统一…

2026-03-11

基础组件库短期内无法被AI取代。业务需要全局设计风格统一,边界兼容性问题AI考虑不全,超复杂逻辑AI考虑不全,组件复用利于降低包体积、提升性能,性能问题AI考虑不全。上面所有问题都考虑全面费token。

其实td那么多未解决的issue,是不是已经违背了“凡事…

2026-03-11

其实td那么多未解决的issue,是不是已经违背了“凡事有交代,件件有着落,事事有回音”

少在issue底下说话,多在pr下说话。前者多面向外部用…

2026-03-11

少在issue底下说话,多在pr下说话。前者多面向外部用户,后者多面向内部开发者。

流程图

2026-03-04

Button 渐变(transition)效果分析

根本原因

button.less 第 250 行定义了:

css
transition: all 0.3s;

这个 transition: all 是一切问题的根源。它意味着 该元素上所有可动画 CSS 属性的变化都会有 0.3 秒的过渡动画,包括 colorbackground-colorborder-coloropacity 等。

触发链路

disabled 属性发生变化时,触发路径如下:

具体来说:

  1. Vue 组件层button.vuewatch 监听 disabled 变化 → 调用 setClass() → 动态添加/移除 t-button--disabled 类名

  2. CSS 层t-button--disabled 类名会改变以下属性(以 primary 主题为例):

    • color: 正常色 → @button-primary-disabled-color
    • background-color: 正常色 → @button-primary-disabled-bg@brand-color-disabled
    • border-color: 正常色 → @button-primary-disabled-border-color
  3. 过渡效果:由于 transition: all 0.3s 的存在,上述 color/background/border 的变化会在 0.3 秒内平滑过渡,产生渐变效果

为什么说是"突变"

Issue #4291 描述的应该是:这个渐变效果本身就不符合预期。对于 disabled 状态的切换,用户期望的是瞬间变为禁用态外观,而不是看到颜色慢慢渐变过去。这里所谓的"突变"可能是指:

  • 在某些场景下(比如点击后立即 disable),用户能明显感知到颜色从正常态"慢慢褪色"到禁用态,体验不佳
  • transition: all 过于粗暴,它不应该影响 disabled 状态切换,本意只是为了 hover 按压态的过渡效果

解决方案

transition: all 0.3s 应该限定作用范围,而不是对所有属性生效。有两种方式:

方案一:精确指定 transition 属性(推荐)

less
// 只对 hover 按压态需要的属性做过渡
transition: background-color 0.3s, border-color 0.3s;

方案二:在 disabled 类上覆盖 transition

less
&.@{button}--disabled {
  transition: none;  // 禁用态不需要过渡动画
  // ...existing styles...
}

方案三:结合两者

less
.@{button} {
  transition: background-color 0.3s, border-color 0.3s;

  &.@{button}--disabled {
    transition: none;
  }
}

小结

属性说明
问题根源transition: all 0.3s(button.less L250)
触发方式disabled / loading / theme 等属性变化 → setClass() 动态切换类名 → CSS 属性变化被 transition 捕获
影响范围所有 button 状态切换(disabled、loading、theme 切换等)都会有 0.3s 渐变
本意仅为 hover 按压态提供平滑过渡
建议transition: all 改为精确属性,或在 disabled 类上设置 transition: none

vue2 hx 模式要修改 index.html 中的 …

2026-02-27

vue2 hx 模式要修改 index.html 中的 favicon.icon、language 等,需要在 manifest.json 中配置 h5.template,否则不生效。

参考 https://uniapp.dcloud.net.cn/collocation/manifest.html#h5

tdesign-uniapp-starter-vue3-…

2026-02-26

tdesign-uniapp-starter-vue3-hx 和 tdesign-uniapp-starter-vue2-hx 的预览应该也要上传。在 HBuilderX 中进行打包,切换到 docs 分支,将刚才的产物提交、push 即可。

链接分享

2026-02-26

https://github.com/Tencent/tdesign-miniprogram/pull/4282 form的样式,小程序并不稳定,要以 uniapp 自己的判断为准,miniprogram 的更新作为参考,校验完成后才能合。

favicon.ico 需要使用远程的,而不是 stat…

2026-02-26

favicon.ico 需要使用远程的,而不是 static 目录下的,否则 GitHub pages 部署后找不到。

diff
+ <link rel="icon" href="https://tdesign.tencent.com/favicon.ico" />
- <link rel="icon" href="/static/favicon.ico" />

图片笔记

2026-02-26

几个模板项目。

topic,包含 tdesign/tdesign-uniapp/uniapp/template/template-project/vue2/hbuilderx 等。

这个 MCP 确实有两个明显的问题:

2026-02-22

这个 MCP 确实有两个明显的问题:

问题一:没有生成中间文档(缺少文档预处理/中间层)

当前的 get-component-docs 工具(docs.ts)和 get-component-source 工具(source.ts)都是 直接把原始的 Markdown 文档或 Vue 源码原封不动返回 给 LLM 的。

press-button 为例,文档有 12KB,如果用户一次查询多个组件,比如表单页面涉及 press-field(14KB)、press-form(18KB)、press-button(12KB)等,token 消耗会非常大。

应该做的:在构建阶段预生成一份精简的「中间文档」,比如:

  • 提取组件的 Props 表格、Events、Slots 等结构化信息
  • 去掉示例代码中的冗余部分
  • 生成一份 JSON 格式的 API 摘要

这样 LLM 消耗的 token 少,响应更快也更准确

问题二:读取本地文件,NPM 包发布后无法使用

当前所有数据来源都是 读取本地文件系统

typescript
// helpers/index.ts
export function getDocsDir(): string {
  return path.join(getProjectRoot(), 'docs/components/press');
}

export function getPackagesDir(): string {
  return path.join(getProjectRoot(), 'src/packages');
}

get-component-listfs.readdir 读目录、get-component-docsreadLocalFile 读 Markdown、get-component-sourcereadLocalFile 读 Vue 文件。

但看 package.jsonfiles 字段只包含 ["dist", "README.md"]文档和源码根本不会被打包进 npm 发布物里。也就是说:

  • 本地开发时可用(因为文档和源码就在旁边的目录里)
  • 通过 NPM 安装后完全不可用dist/ 目录里不包含文档和源码,读取路径全部失效)

建议方案

有几种修复思路,你看想用哪种:

方案 A:构建时内嵌(推荐)

rollup 构建阶段,把文档和源码的内容预处理后内嵌到 JS 产物中(比如生成一个 data.json),打包进 dist/。这样 NPM 发布后也能用,同时也顺带解决了中间文档的问题——可以在构建时做精简。

方案 B:远程获取

把文档和源码部署到 CDN 或 API 服务,MCP 通过 HTTP 请求获取,而不是读取本地文件。

方案 C:把文档也打包进 files

修改 package.jsonfiles 字段,把 docs/src/packages/ 也包含进去,但这样会让包体积很大,而且还是没有中间文档精简的能力。

mermaid 代码片段

2026-02-14

CDN 地址维护者上传方式
tdesign.gtimg.com/mcp/本项目手动上传
static.tdesign.tencent.com/TDesign 官方CI 自动部署
tdesign.gtimg.com/site/TDesign 官方CI 自动部署

6. 组件列表 https://tdesign.gtim

2026-02-14

  1. 组件列表 https://tdesign.gtimg.com/mcp/web-components.json

1. 组件列表 https://tdesign.gtim

2026-02-14

  1. 组件列表 https://tdesign.gtimg.com/mcp/tdesign-vue-next/index.json
  2. dom 结构 https://tdesign.gtimg.com/mcp/tdesign-web-dom/avatar.html
  3. 文档 https://tdesign.gtimg.com/mcp/tdesign-vue-next/button/api.md
  4. 示例 https://tdesign.gtimg.com/mcp/tdesign-react/button/Demo.tsx,https://tdesign.gtimg.com/mcp/tdesign-vue-next/button/Demo.vue
  5. changelog https://static.tdesign.tencent.com/vue-next/changelog.json

plugin-light 中的类型修复,主要是在 tsc…

2026-02-12

plugin-light 中的类型修复,主要是在 tsconfig.json 中配置 src/**/*.ts,或者 ./**/*.d.ts ,默认是继承自根目录下的 tsconfig.json,需要排除其他的。

两者的区别

2026-02-11

两者的区别

对比项lib/index.esm.jses/index.mjs
打包方式单文件打包(所有代码打包在一起)保留源码结构(多文件)
文件大小较大(包含所有依赖)较小(入口文件)
依赖处理内部依赖已打包依赖其他 .mjs 文件
加载方式加载一个文件即可需要加载多个文件
Tree Shaking支持(但已打包)支持(更细粒度)

潜在影响

优点:

  • 🌲 更好的 Tree Shaking:打包工具可以更精确地按需引入
  • 📦 更小的初始体积:只加载入口文件,其他按需加载
  • 🔧 更灵活的优化:打包工具可以对每个模块单独优化

缺点:

  • ⚠️ 依赖关系复杂:需要确保 es/ 目录下的所有文件都正确发布
  • ⚠️ 兼容性问题:某些老旧的打包工具可能不支持 .mjs 扩展名
  • ⚠️ 文件数量多:需要发布整个 es/ 目录

📊 产物对比表

2026-02-11

📊 产物对比表

产物格式目录结构文件数量使用场景
lib/*.jsCommonJS保留多文件Node.js 环境
lib/index.esm.jsES Module单文件1 个现代打包工具(单文件)
es/*.mjsES Module保留多文件现代打包工具(按需引入)

之前运行时的引入方式是 ,包体积大小:

2026-02-11

之前运行时的引入方式是 import { xxx } from 't-comm/lib/xx/xx',包体积大小:

t-comm 改造,支持浏览器环境。业务中使用 import { xxx } from 't-comm' 后,包体积增加了 30KB

尝试将 package.json 中的 modulelib/index.esm.js 改成 es/index.mjs 后,包体积恢复之前的大小:

为什么 没问题, 在浏览器中就有问题,是 做了什么吗

2026-02-11

为什么 import * as fs from 'fs' 没问题,import { execSync, spawnSync } from 'child_process'; 在浏览器中就有问题,是 vite 做了什么吗

核心原因如下。

fschild_process 都是 Node.js 内置模块,它们在浏览器环境中都是不存在的。但为什么表现不同呢?这主要是因为 Vite(以及底层的 Rollup/esbuild)对不同模块的处理策略不同

  1. fs 模块的处理

当你在浏览器代码中 import * as fs from 'fs' 时:

  • Vite 会自动将 fs 模块"空化"(shimmed/polyfilled)
  • 对于 fs 这类常见的 Node.js 模块,Vite/Rollup 默认会提供一个空的 polyfill
  • 导入不会报错,但 fs 对象会是一个空对象 {},调用任何方法都会失败

你可以验证:

ts
import * as fs from 'fs';
console.log(fs); // 在浏览器中会输出 {} 或 undefined
  1. child_process 模块的处理

child_process 模块:

  • Vite 默认不会为它提供 polyfill
  • 当你尝试导入时,打包工具找不到这个模块,就会直接报错
  • 这是因为 child_process 涉及系统进程操作,根本无法在浏览器中模拟
  1. Vite 的具体处理机制

Vite 使用了 @rollup/plugin-node-resolve 插件,它有一个 browser 字段处理逻辑:

js
// Vite 内部大致的处理逻辑
{
  // 这些模块会被替换为空模块
  "fs": false,        // 被 shimmed 为空
  "path": "path-browserify",  // 有 polyfill
  "child_process": undefined  // 没有处理 → 报错
}
  1. 为什么这样设计?
模块浏览器 Polyfill原因
fs空对象 {}某些库可能条件引入,空化可以避免打包失败
pathpath-browserify路径操作可以在浏览器模拟
child_process❌ 无进程操作完全无法模拟,直接报错更清晰

解决方案

如果你的代码中确实需要在浏览器中引用这些模块(比如共享代码),可以在 vite.config.ts 中配置:

ts
export default defineConfig({
  resolve: {
    alias: {
      // 将 child_process 也替换为空模块
      'child_process': 'rollup-plugin-node-polyfills/polyfills/empty'
    }
  },
  // 或者使用 define 将其标记为外部模块
  define: {
    'process.env': {}
  }
})

或者使用条件导入:

ts
// 只在 Node.js 环境中导入
if (typeof window === 'undefined') {
  const { execSync } = await import('child_process');
}

总结

问题答案
fs 为什么不报错?Vite 默认将其 shimmed 为空对象
child_process 为什么报错?Vite 没有为它提供 polyfill
是 Vite 做的吗?是的,Vite + Rollup 的模块解析策略决定的

- 难点,逆转化,多端适配,充分测试,e2e测试

2026-02-10

  • 难点,逆转化,多端适配,充分测试,e2e测试
  • Press UI 和 TDesign Uniapp 有哪些业务在使用,知名业务,影响力
  • 带过哪些人,有8/9级的吗

pnpm create uni 本质上是 pnpm ex…

2026-02-08

pnpm create uni 本质上是 pnpm exec create-uni 的简写,npm/pnpm/yarn 都遵循一个通用约定:

[包管理器] create <name> ≈ [包管理器] exec create-<name>

也就是说,当你执行 pnpm create uni 时,包管理器会自动:

  1. 检查本地是否有 create-uni 包,没有则临时安装
  2. 执行 create-uni 包中的可执行脚本(一般在 package.json 的 bin 字段定义)
  3. 脚本执行脚手架的核心逻辑(拉取模板、初始化项目、交互配置等)

图片笔记

2026-02-06

活动报名模板

图片笔记

2026-02-06

社区内容模板、零售电商模板、组件库模板

流程图

2026-02-05

好的,这是将消息流程图转换为 Mermaid 格式:

或者使用流程图格式:

你可以根据需要选择使用时序图(sequenceDiagram)或流程图(flowchart)格式。

- 走账号默认行为(可能是新面板也可能是旧面板)

2026-02-04

  • 走账号默认行为(可能是新面板也可能是旧面板)
  • 强制打开旧面板进行分享:页面访问需携带参数 _nsp=2
  • 强制打开新面板进行分享:页面访问需携带参数 _nsp=1
  • 如上场景下,切换不同的自定义分享信息正常

图片笔记

2026-02-04

Vue2 中数组类型的 prop,也要用函数返回。

Vue2 组件中尽量不要用 _icon,_value, …

2026-02-03

Vue2 组件中尽量不要用 _icon,_value, _placement,一是有警告问题,二是有逻辑或显示问题,比如 guide 中对 _current 的 watch 失效,result 中 _icon 无法显示

语法对比

2026-02-03

语法对比

写法Vue 2Vue 3
v-model:visible="visible"❌ 不支持✅ 支持
:visible.sync="visible"✅ 支持❌ 已移除
:visible="visible" @update:visible="..."✅ 支持✅ 支持

使用 :visible + @update:visible 的手动绑定方式可以同时兼容 Vue 2 和 Vue 3

javascript 代码片段

2026-02-03

javascript
newProps[key] = {
  type: [null, ...newType],
  default: null,
};

在 Vue 2 中,type 数组中包含 null 会导致 Right-hand side of 'instanceof' is not an object 错误,因为 Vue 2 会对 type 数组中的每个元素执行 instanceof 检查,而 null 不是一个有效的构造函数。

解决方案:将 nulltype 数组中移除,只保留有效的类型构造函数。如果需要允许 null 值,可以通过 default: nullvalidator 来处理。

已修复!将第 166 行的:

javascript
type: [null, ...newType],

修改为:

javascript
type: newType,

原因说明

  • Vue 2 在进行 prop 类型检查时,会遍历 type 数组并对每个元素执行 instanceof 检查
  • null 不是一个有效的构造函数,所以 value instanceof null 会抛出 Right-hand side of 'instanceof' is not an object 错误
  • Vue 3 对此做了处理,允许 type 中包含 null,但 Vue 2 不支持

修复后,default: null 仍然保留,这样 prop 的默认值依然是 null,只是类型检查时不再包含 null

修复说明

2026-02-01

修复说明

  1. 移除了 "type": "module"

当同时提供 ESM 和 CommonJS 两种格式时,不应设置 type: module,而应通过 exports 字段来声明不同格式的入口。

  1. 修正了入口文件路径
  • main: dist/index.jsdist/index.cjs(CommonJS 入口)
  • module: dist/index.mjsdist/index.js(ESM 入口,实际文件名)
  1. 添加了 exports 字段
json
"exports": {
  ".": {
    "import": {
      "types": "./dist/types/index.d.ts",
      "default": "./dist/index.js"
    },
    "require": {
      "types": "./dist/types/index.d.ts",
      "default": "./dist/index.cjs"
    }
  },
  "./dist/index.css": "./dist/index.css",
  "./style": "./dist/index.css"
}

这样 Node.js 和打包工具会根据导入方式自动选择正确的模块格式:

  • ESM 项目(import)→ 使用 dist/index.js
  • CJS 项目(require)→ 使用 dist/index.cjs
  1. 添加了 sideEffects 字段
json
"sideEffects": [
  "dist/index.css",
  "**/*.css"
]

告知打包工具 CSS 文件有副作用,防止被 tree-shaking 错误删除。

- 正向先行断言,匹配后面紧跟pattern的位置,?=…

2026-01-30

  • 正向先行断言,匹配后面紧跟pattern的位置,?=pattern
  • 负向先行,匹配后面不紧跟…?!pattern
  • 正向后行,匹配前面…?<pattetn
  • 负向后行,匹配…?<!pattern

通过 scp 同步服务 nginx 配置,下载下来,改好…

2026-01-30

通过 scp 同步服务 nginx 配置,下载下来,改好,再上传,还有 git 记录。

sh 代码片段

2026-01-29

sh
pip3 install lizard -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
sh
lizard  -x "**/node_modules/*"

python2 装这个版本

sh
pip install lizard==1.17.10

pandoraShowEntrance

2026-01-29

pandoraShowEntrance

css 尽量复用小程序端的

2026-01-29

css 尽量复用小程序端的

  1. 小程序端和uniapp端样式部分差异小,相同部分远大于不同部分
  2. css 难diff,一行一行的太分散,如果不复用的话,精确同步太费时间

既然要复用CSS

  1. 其衍生出的文档中的 CSS 变量部分也要复用,或者生成变量的脚本复用
  2. CSS 复制不是一次性工作,所以 uniapp 差异部分不要放在同一个文件里,单独拿出来或放到 vue 文件中

td-uniapp 样式处理

2026-01-29

td-uniapp 样式处理

  • 执行 notes/scripts/td/copy-less-files.js

不管分销转换产品、游戏还是其他维度,关键词TIP_STY…

2026-01-29

不管分销转换产品、游戏还是其他维度,关键词TIP_STYLE_NAME可以代替任何东西,凡是需要编译时进行单独打包的都可以用这个。

pixui 中使用 vConsole 的卡点

2026-01-29

pixui 中使用 vConsole 的卡点

  • parentElement
  • initCustomEvent
  • css variables
js
return new CustomEvent(type, {
    detail,
    bubbles,
    cancelable
  })

图片笔记

2026-01-29

tsconfig.json用根目录的。

tdesign-miniprogram 依赖版本太低。

目前的构建包

2026-01-28

目前的构建包

包名作用
cherry-markdown.js
cherry-markdown.min.js
完整包,较大,包含cherry所有功能(工具栏、左侧编辑器、右侧预览器)
cherry-markdown.core.js核心包,相比完整包,只少了mermaid功能,包大小小了50%以上(当然也可以在引入核心包后再传入mermaid,从而实现对mermaid的支持)
cherry-markdown.engine.core.js解析引擎包,可以理解为只提供了将md解析成html的api

目标

在纯流式渲染的场景下,每个包都很大,希望再提供一个针对纯流式渲染场景的构建包,包里去掉:1、editor(编辑器组件,但可能要保留一个textarea)、toolbar(工具栏组件,包括顶部工具栏、侧边栏、悬浮目录)、mermaid(这个包太大了,引导用户自己引入就好了)

包名大概为:cherry-markdown.stream.js

确认排除的依赖:

  • mermaid - 通过 rollup.stream.config.js 的 external 配置排除
  • codemirror - 通过 rollup.stream.config.js 的 external 配置排除
  • mathjax/katex - 没有打包进去,但在 Engine.js 中会动态加载(通过配置的 src 和 css)

tdesign-vue-next/chat 中 用了 …

2026-01-28

tdesign-vue-next/chat 中 chat-markdown 用了 tdesign-web-components 中的 chat-message/content/markdown-content,后者又用了 cherry-markdown 中的 dist/addons/cherry-code-block-mermaid-plugindist/cherry-markdown.core,不止是 engine,是 core!

流程图

2026-01-28

packages/cherry-markdown/src/core 目录详解

📁 目录结构

core/
├── HookCenter.js          # 语法钩子注册中心
├── HooksConfig.js         # 默认钩子配置列表
├── SyntaxBase.js          # 行内语法基类
├── ParagraphBase.js       # 块级语法基类
├── SentenceBase.js        # 句子级钩子基类(已弃用)
└── hooks/                 # 具体语法钩子实现
    ├── 块级语法 (22个)
    └── 行内语法 (15个)

🎯 核心文件详解

  1. HookCenter.js - 语法钩子注册中心

作用:管理所有 Markdown 语法钩子的注册、分类和优先级

核心功能

方法说明
registerInternalHooks()注册系统内置的语法钩子
registerCustomHooks()注册用户自定义的语法钩子
register()实际注册一个钩子实例
getHookList()获取所有钩子(按类型分组)

关键逻辑

javascript
// 钩子分为两类
this.hookList = {
  sentence: [],  // 行内语法钩子(如加粗、斜体)
  paragraph: [], // 块级语法钩子(如标题、代码块)
};

自定义钩子支持

  • 可以指定 before/after 插入位置
  • 可以设置 force: true 覆盖同名内置钩子

  1. HooksConfig.js - 默认钩子配置

作用:定义所有内置语法钩子的加载顺序

执行顺序规则

  • beforeMakeHtml:按数组顺序正序执行
  • makeHtml:按数组顺序正序执行
  • afterMakeHtml:按数组顺序逆序执行

钩子加载顺序

javascript
const hooksConfig = [
  // === 块级语法(先处理) ===
  FrontMatter,     // YAML 前置元数据
  CodeBlock,       // 代码块 ```
  InlineCode,      // 行内代码 `
  InlineMath,      // 行内公式 $
  MathBlock,       // 块级公式 $$
  AiFlowAutoClose, // AI 流式输出自动闭合
  HtmlBlock,       // HTML 块
  Footnote,        // 脚注 [^1]
  CommentReference,// 注释引用
  Transfer,        // 转义字符
  Br,              // 换行
  Table,           // 表格
  Toc,             // 目录
  Blockquote,      // 引用 >
  Header,          // 标题 #
  Hr,              // 水平线 ---
  List,            // 列表
  Detail,          // 折叠块 <details>
  Panel,           // 面板
  Paragraph,       // 普通段落

  // === 行内语法(后处理) ===
  Emoji,           // 表情 :smile:
  Image,           // 图片 ![]()
  Link,            // 链接 []()
  AutoLink,        // 自动链接
  Emphasis,        // 强调 *斜体* **粗体**
  BackgroundColor, // 背景色
  Color,           // 文字颜色
  Size,            // 字体大小
  Sub,             // 下标
  Sup,             // 上标
  Ruby,            // 注音
  Strikethrough,   // 删除线
  Underline,       // 下划线
  HighLight,       // 高亮
  Suggester,       // @ 提及
  Space,           // 连续空格
];

  1. SyntaxBase.js - 行内语法基类

作用:所有行内语法钩子的基类(如加粗、斜体、链接)

生命周期方法

javascript
class SyntaxBase {
  // 在主渲染前预处理
  beforeMakeHtml(str) { return str; }

  // 核心渲染方法:Markdown → HTML
  makeHtml(str) { return str; }

  // 渲染后处理
  afterMakeHtml(str) { return str; }

  // 测试字符串是否匹配当前语法
  test(str) { return this.RULE.reg.test(str); }

  // 定义匹配规则(子类必须重写)
  rule(editorConfig) {
    return { begin: '', end: '', content: '', reg: new RegExp('') };
  }
}

类型定义

javascript
export const HOOKS_TYPE_LIST = {
  SEN: 'sentence',    // 行内语法
  PAR: 'paragraph',   // 块级语法
  DEFAULT: 'sentence',
};

  1. ParagraphBase.js - 块级语法基类

作用:所有块级语法钩子的基类(如标题、代码块、表格)

与 SyntaxBase 的区别

特性SyntaxBaseParagraphBase
类型sentenceparagraph
缓存机制
换行处理
行号计算

缓存机制

javascript
// 缓存用于提升性能,避免重复渲染
pushCache(str, sign, lineCount)  // 存入缓存
popCache(sign)                   // 取出缓存
restoreCache(html)               // 还原所有缓存
checkCache(wholeMatch, ...)      // 检查是否命中缓存

缓存键格式

~~C${cacheCounter}I${sign}_L${lineCount}$
例如:~~C0Iabc123_L5$

换行处理

javascript
// 经典模式 vs 现代模式
this.classicBr = true;  // 一个换行被忽略,两个换行分段
this.classicBr = false; // 一个换行变<br>,两个换行分段

  1. SentenceBase.js - 句子级基类(已弃用)

作用:早期版本的钩子基类,现已基本弃用

javascript
class HookBase {
  getType() {
    const typeList = { 1: 'sentence', 2: 'paragraph', 3: 'page' };
    return typeList[this.HOOKTYPE] || 'sentence';
  }
}

📂 hooks/ 子目录 - 具体语法实现

块级语法钩子(22个)

文件钩子名语法示例说明
Header.jsheader# 标题支持 ATX(#)和 Setext(===)两种风格
CodeBlock.jscodeBlock```js支持语法高亮、行号、复制、展开、自定义渲染器
Table.jstable|a|b|支持对齐、图表渲染(ECharts)
List.jslist- item支持有序、无序、任务列表、多种样式
Blockquote.jsblockquote> 引用引用块
MathBlock.jsmathBlock$$ ... $$块级数学公式(MathJax/KaTeX)
Footnote.jsfootnote[^1]脚注
Toc.jstoc[[toc]]自动生成目录
Hr.jshr---水平分割线
Br.jsbr换行换行处理
HtmlBlock.jshtmlBlock<div>HTML 块级元素
FrontMatter.jsfrontMatter---\nyaml\n---YAML 元数据
Panel.jspanel自定义面板信息/警告/错误面板
Detail.jsdetail<details>可折叠内容
Paragraph.jsparagraph普通文本普通段落(兜底)
CommentReference.jscommentReference[ref]: url全局引用定义
Transfer.jstransfer\*转义字符处理
AiFlowAutoClose.jsaiFlowAutoClose-AI 流式输出自动闭合
InlineCode.jsinlineCode`code`行内代码(在块级处理)
InlineMath.jsinlineMath$x^2$行内公式(在块级处理)

行内语法钩子(15个)

文件钩子名语法示例说明
Emphasis.jsfontEmphasis**粗体** *斜体*支持 * 和 _ 两种符号
Image.jsimage!alt支持扩展属性、视频/音频
Link.jslinktext支持 target 属性
AutoLink.jsautoLinkhttps://...自动识别 URL
Strikethrough.jsstrikethrough~~删除~~删除线
Underline.jsunderline-下划线
HighLight.jshighLight==高亮==文字高亮
Color.jscolor-文字颜色
BackgroundColor.jsbackgroundColor-背景颜色
Size.jssize-字体大小
Sub.jssubH~2~O下标
Sup.jssupX^2^上标
Ruby.jsruby-注音(ruby 标签)
Emoji.jsemoji:smile:表情符号
Suggester.jssuggester@user@ 提及/智能建议
Space.jsspace多个空格连续空格保留
SuggestList.js--Suggester 的辅助模块
Emoji.config.js--Emoji 配置数据

🔄 语法解析流程


🛠️ 如何创建自定义语法钩子

  1. 行内语法示例
javascript
import SyntaxBase from '@/core/SyntaxBase';

class MyInlineHook extends SyntaxBase {
  static HOOK_NAME = 'myInline';

  makeHtml(str) {
    return str.replace(/\[\[(.+?)\]\]/g, '<mark>$1</mark>');
  }

  rule() {
    return {
      begin: '\\[\\[',
      content: '(.+?)',
      end: '\\]\\]',
      reg: /\[\[(.+?)\]\]/g,
    };
  }
}
  1. 块级语法示例
javascript
import ParagraphBase from '@/core/ParagraphBase';

class MyBlockHook extends ParagraphBase {
  static HOOK_NAME = 'myBlock';

  constructor() {
    super({ needCache: true }); // 启用缓存
  }

  makeHtml(str, sentenceMakeFunc) {
    return str.replace(/:::(\w+)\n([\s\S]+?)\n:::/g, (match, type, content) => {
      const { html } = sentenceMakeFunc(content);
      return this.pushCache(`<div class="${type}">${html}</div>`, this.sign);
    });
  }
}

📊 核心类继承关系


📝 总结

core 目录是 Cherry Markdown 的语法解析核心

  1. HookCenter - 统一管理所有语法钩子的注册和调度
  2. HooksConfig - 定义语法处理的优先级顺序
  3. SyntaxBase - 行内语法的基类(轻量、无缓存)
  4. ParagraphBase - 块级语法的基类(支持缓存、行号计算)
  5. hooks/ - 37 个具体语法实现

设计亮点

  • 🔌 插件化:支持自定义语法扩展
  • 高性能:块级语法支持缓存
  • 🎯 优先级:通过配置数组控制处理顺序
  • 🔄 生命周期beforeMakeHtmlmakeHtmlafterMakeHtml

mermaid 代码片段

2026-01-28

方法永远可以做到不私密,如果存在私密的方法,那么可以拆分…

2026-01-28

方法永远可以做到不私密,如果存在私密的方法,那么可以拆分出不私密的方法和私密的变量。

真正私密的永远是配置信息、密钥等常量,所以上面的方式永远有效。

plugin-light-const 的定位:

2026-01-28

plugin-light-const 的定位:

  1. 放配置信息、常量定义,比如 getCdnList
  2. 有点私密,不方便放 t-comm 里
  3. 如果是需要运行时和编译时都需要的函数,放到 t-comm 里,而不是 project-config-const 中

链接分享

2026-01-27

https://github.com/dcloudio/uni-app/issues/3793 这个评论不错,提到了 rpx 在uniapp H5 中的转换

要验证 PR 的改动(pkg.pr.new),或者 np…

2026-01-26

要验证 PR 的改动(pkg.pr.new),或者 npm 包内容

  1. 进入工程,cd packages/tdesign-uniapp/example
  2. 去掉 vite.config.tsalias 的配置
  3. 装包,如 pnpm i https://pkg.pr.new/Tencent/tdesign-miniprogram/tdesign-uniapp@4201
  4. 执行 dev 等命令,如 npm run dev:h5

流程图

2026-01-26

demo 同步

一次性工作。

这部分是从 vue3-cli 同步到 app/vue2-cli 等目录中的。

需要监听的部分,主要是组件和示例,组件目标是 _tdesign,或者 uni_modules/tdesign-uniapp 下。

这部分是从 uniapp-components 等同步到 vue3-cli/app/vue2-cli 等目录中的。

每个项目独特的部分

小程序长按图片,保存图片没反应?

2026-01-26

小程序长按图片,保存图片没反应?

原因是没返回签名地址,比较坑的是没有提示。

- https://github.com/Tencent…

2026-01-26

这两个还要再看下

td-mini 同步 td-uniapp 的步骤:

2026-01-26

td-mini 同步 td-uniapp 的步骤:

  1. 可选,在 td-mini 大仓下进行 build 脚本的改造,去掉 jsmin/jsonmin/wxmlmin 的使用
  2. 执行 npm run build(或者 npm run build -- --ignore-terser),生成 _example 目录
  3. 复制 _example 目录到 mini-to-uni 工程下,进行覆盖
  4. 可选,删除之前的 _example_uni
  5. mini-to-uni 工程下执行 node ./bin/wtu -i ./_example 进行 uniapp 组件生成
  6. 手动 diff,结合 PR,Git 记录,更新 td-uniapp 组件库

需要加上 ,否则边框位置不对。

2026-01-26

1.t-grid-item__content--left 需要加上 width: 100%;box-sizing: border-box;,否则边框位置不对。

要将所有的 :deep 改成 custom-style,…

2026-01-26

要将所有的 :deep 改成 custom-style,工作量有点大,退而求其次,只在组件 less 中加 :deep,不加、不删、不改其他样式。有改动的,记录下来,比如 dialog.less 的改动如下:

其实用 也是有兼容性问题的,Vue2 需要换,不如直接…

2026-01-25

其实用 :deep(xx) 也是有兼容性问题的,Vue2 需要换,不如直接用 customStyle

packages/tdesign-uniapp/app/…

2026-01-25

packages/tdesign-uniapp/app/ 待删除

为什么小程序样式覆盖需要用 ,而 H5 不需要?

2026-01-25

为什么小程序样式覆盖需要用 :deep,而 H5 不需要?

原因是 H5 中节点会合并,或者说会替换成真正的子组件节点,可以看到下面的 uni-button 有两个 data-v-xx,而小程序不是。

贡献指南;mini-to-uniapp commit

2026-01-25

贡献指南;mini-to-uniapp commit

td-uniapp 的难点,一是宏观,架构搭建、监听体系…

2026-01-25

td-uniapp 的难点,一是宏观,架构搭建、监听体系、更新策略,二是微观,又可分为实现原理和细节。实现上,对几十个组件了如指掌、如数家珍,不同端的兼容性、差异性有不同的处理策略,细节上,对每个组件的还原效果、深色模式、色值等效果对齐,抠每一处细节。

vue2+cli/vue3+cli/vue2+hx/vu…

2026-01-25

vue2+cli/vue3+cli/vue2+hx/vue3+hx 组件基础示例,vue3+cli/vue3+hx 社区模板;chat mr 合入;eslint问题;src/api合入

今日已同步 td-mini 最新改动 v1.12.2(2…

2026-01-25

今日已同步 td-mini 最新改动 v1.12.2(2026-01-21)。不含 chat。

td-uniapp 中的示例页面,加上 demo-nav…

2026-01-25

td-uniapp 中的示例页面,加上 demo-navbar 类名,就是白底黑色,否则就是透明底默认颜色。

css
.demo-navbar {
  --td-navbar-bg-color: var(--td-bg-color-container);
  --td-navbar-color: var(--td-text-color-primary);
}

这个 issue 有意思,https://github.…

2026-01-25

这个 issue 有意思,https://github.com/Tencent/tdesign-miniprogram/issues/3986

ts
export function getMonthByOffset(date, offset) {
  const _date = new Date(date);
  _date.setMonth(_date.getMonth() + offset);
  return _date;
}

getMonthByOffset(value, n),如果 value + n 月那一天没有 dd, 则会自动进入下一个月,也就是value+n+1。比如 10月31日 + 1月,会被处理成 12月,正常应该是 11 月。

流程图

2026-01-20

批量发布流程

批量发布的核心字段 batchUpload,1. ser…

2026-01-20

批量发布的核心字段 batchUpload,1. server 传给流水线、流水线再回传给 server 2. 操作日志 operation 的 batchUpload 只做留存,无实际作用

操作记录 operation 的设计哲学应该是 1 有唯…

2026-01-20

操作记录 operation 的设计哲学应该是 1 有唯一的 pipelineId、pipelineRunId,且不应该变化 2. 不同类型的操作日志应该有统一的字段,类似于抽象类。

将 regionSelect 的 regionOptio…

2026-01-17

将 regionSelect 的 regionOptions 改为受控,当搜索时,之前的 selected 不在 regionOptions 时,就更新 selected。这样可以在 搜到一个值,不点选择,直接点外层确定也不会违反直觉。

search 输入框和 select 的回显用一个,即都是 input,这个之前的逻辑不变。

handleSelect 时,更新 searchValue 为选中的值,这个之前的逻辑不变。

展示 dropdown 时,清空 searchValue,来展示所有 regionOptions,这个之前的逻辑不变。

monorepo 仓库的每次提交都应该只改动一个子包的,…

2026-01-15

monorepo 仓库的每次提交都应该只改动一个子包的,让 commit 信息更聚焦,生成的 changelog 更易读。