作者
novlan1
2026.1.1
TDesign 碎片笔记
harmony 中 countdown 中 cppcrash
2026-06-29
重新分析
关键事实:
- 崩溃时
Process life time: 20s,进入前台 15 秒就崩; Page switch history显示没有页面销毁过(Page:2、Page:3 都还活跃,只是切到 count-down 页时叠在第 3 页);- 堆栈是
Builtins_ProxySetProperty→SetKeyedProperty→JumpIfToBooleanFalseHandler时崩溃; - demo 页同时开了 20 多个 count-down 实例,其中 6 个是 millisecond 模式(每 33ms 一次 setTimeout 写 reactive 数据);
- 寄存器 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 对象,只在需要时才更新。
我先检查一下 parseTimeData、parseFormat、uniComponent 包装:
好,现在分析得更清楚了。每次 updateTime 都会:
- 创建新对象
parseTimeData(remain)并赋给this.timeData(reactive set,子属性 5 个); - 创建新数组
format.split(':')赋给this.timeRange(reactive set); - 修改
this.formattedTime(reactive set); - 修改
this.remain(reactive set); $emit('change', timeData)—— 走 Vue 事件系统。
毫秒模式下每 33ms 一轮,6 个毫秒实例 × 5+ 个 reactive set/轮 = 每秒近 1000 次 reactive proxy.set,鸿蒙 V8 在这种压力下会触发 GC race。
修复方案
我做以下几件事:
- 降低 millisecond 模式频率:从 33ms 改为 100ms(人眼 10fps 已经够看了);
- 避免不必要的 reactive 写入:
this.timeRange不是数据,不需要响应式更新——它由format决定,且format是 prop。可以放进 computed 或mounted里只算一次,避免每帧重写;parseTimeData返回的对象只在毫秒模式才需要每帧更新,普通秒模式只在秒变化时改;- 减少
$emit('change')频率:和updateTime的 reactive 更新一起,按秒触发即可(毫秒模式继续保持每帧)。
- 使用
Object.freeze包裹timeData:让 Vue3 不要给内部 5 个属性加 reactive proxy(每次都是新对象,没必要做深响应式,节省大量 Proxy.set)。 - 保留之前的
_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 的引擎层问题,到那时建议:
- 临时方案:把 demo 里 6 个 millisecond 实例改成 v-if 控制,只展示 1 个,或暂时屏蔽 millisecond demo;
- 彻底方案:在count-down.vue 中改造为 全局单一 ticker 派发(所有 count-down 实例共用一个 setInterval,在 raf 节奏内统一更新),把全局 setTimeout 数量从 N 个降到 1 个;
- 或者升级华为系统版本 / DevEco IDE。
请重新打包验证一下,看看新日志还会不会出现 SIGSEGV。
获取 TDesign 所有侧边栏链接,看看数量之类的
2026-06-21
// 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 } 模式
.t-icon-check::before { content: '\e001'; }<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 子元素上不会按字号撑开。
解决:
- 父
.t-icon显式width: 1em; height: 1em撑开 - 子
.t-icon-{name}显式font-size: inherit; width: 1em; height: 1em; display: flex - 子节点也显式
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:
<text v-if="glyphChar" class="t-icon__glyph" :style="customStyle">{{ glyphChar }}</text>把"图标怎么定义"的责任完全交给业务方:你给字符、你给字体族,组件只负责渲染。
📌 思路转折点:从"伪元素 + class 字典"模式 → "字符 + 字体族"模式。
阶段 ③:customStyle 同时挂在两个节点的丑陋设计
问题:上一步为了让 font-family 一定生效,把 customStyle 同时挂在 root view 和 inner text 上:
<view :style="rootStyle + customStyle">
<text :style="customStyle">{{ glyphChar }}</text>
</view>坏处:用户传 padding: 8px 会被应用两次,background 会画两次盒子。语义错乱。
重构 - BEM 修饰符方案:
引入 .t-icon--glyph 修饰符:
.t-icon { font-family: 't'; }
.t-icon--glyph { font-family: inherit; } // ← 在 glyph 模式下重置<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 全局注册字体:
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}) 全局注册字体 |
核心结论
- uvue style 的隔离是"双层"的:选择器隔离(看得见也匹配不到子组件节点)+
@font-facefamily 隔离(family 名字跨不出作用域)。 - 跨组件传字体图标的优雅方案:组件提供
glyphCharprop(character-based 而非 class-based),由 customStyle 注入 font-family,BEM 修饰符 reset 默认字体。 - 跨组件注册字体的标准方案:
uni.loadFontFace({ global: true }),不要靠<style>里的@font-face。 - CSS 层叠规则已足够:inline style > class,同特异度后写胜出。
!important在这个场景没必要,是过度防御。
color-picker 预设色彩没有横向排列
2026-06-18


uniapp x 启动开发
2026-06-18
# 一次性同步
sudo pnpm --filter="./packages/uniapp-x-components" run sync
# 监听同步
sudo pnpm --filter="./packages/uniapp-x-components" run sync:watchuniappx 中的 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-icon | ✅ font-size: 24px(自己写的) | 24px | 24×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,显式声明继承:
each(@icons, {
&-@{key} {
font-family: 't' !important;
font-size: inherit; // 👈 关键:强制从父级 .t-icon 继承字号
line-height: 1;
width: 1em; // 现在 1em = 父级 24px = 24px ✅
height: 1em;
...
}
});验证后效果
- 父级
.t-iconfont-size: 24px → 子级继承 24px → 子盒子 24×24 ✅ - 伪元素
::before字形按 24px 渲染 → 图标尺寸 24 ✅ - 按钮 size 阶梯切换(small/medium/large)改
.t-iconfont-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> 标签:
<view v-else :class="glyphClass" /> <!-- glyphClass = 't-icon-search' -->关键差异(与 uniapp 端对照):
- 浏览器原生 +
<text>标签:行内元素,::beforecontent 字符按 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:
.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 — 两处改动
// 改动 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) — 去硬编码
- <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 显式传
sizeprop → 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 实现的滚轮组件,在快速滑动时能做到逐档实时音效。原因就是你现在遇到的问题:
- 小程序 setData 机制:每次
this.offset = newOffset在小程序里要经过 setData → 视图层 → 渲染,本身有 16ms+ 延迟 - InnerAudioContext 起播延迟:微信小程序首次播放 100~300ms,后续即便复用实例也有 30~80ms 抖动
- uni.vibrateShort 节流:小程序底层强制 30~50ms 防抖
- CSS transform 动画期间无 JS 钩子:惯性段我们用 setTimeout 模拟节奏,但 setTimeout 在小程序逻辑层和视图层渲染线程不同,对不齐
- 音频/振动 API 本身耗时:在低端安卓机,单次
play()调用进 webview-bridge 就要 50ms+
我前面的方案——onTouchMove 跨档检测 + 惯性段缓动 setTimeout 预排——在原理上就是对抗系统物理限制,做得再好也是"用算法逼近",永远做不到真正同步。
你说的"只管最后一步骤"才是真正的工程解
这才是社区的真实做法。仔细想:
| 方案 | 工程复杂度 | 体验 | 卡顿风险 |
|---|---|---|---|
| 逐档实时(当前) | 高 | 看似 iOS,实测延迟 | 高 |
| 只在最终落点响 | 极低 | 干净利落 | 无 |
| 落点响 + 强振动 | 极低 | 触感像 iOS | 无 |
只在最终落点:
- ✅ 代码极简,没有 setTimeout 池、没有抽样、没有惯性预测
- ✅ 不会跟系统抢音频/振动资源
- ✅ 用户感知是"我选完了——咔哒,确认",跟系统 picker 行为一致
- ✅ 振动可以用
type: 'medium'加重,弥补只响一声的"轻量感"
实际上原生 iOS picker 的体验本质上也是:滚动过程中是 SDK 内部的 audio engine 实时跟踪 cell 居中(人体感觉非常快是因为它根本没经过 JS 层),如果你真的实测,你以为的"每档都响"其实是大脑补全的视觉关联——你在快速滑动时根本听不出是 1 声还是 5 声,停下后那一声才是核心反馈。
建议:回退到"只管最后一步"
具体改法(比现在简单 90%):
picker-item.vue:删掉
_lastTickIndex、_inertiaTickTimers、onTouchMove中的跨档检测、onTouchEnd中的缓动 setTimeout 预排。只保留updateSelected(index, true)→ 调用triggerColumnChange(其实代码本来就有,原地不动)picker.vue:把
playTickFeedback调用从triggerColumnTick移到triggerColumnChange。删除抽样逻辑。triggerColumnTick/pick-tick事件保留还是删? 我建议保留作为业务高级钩子(万一未来有人确实想自己做逐档逻辑),但组件内部不再调用playTickFeedback。
结果:
- 滑一档 → 落定 → 1 声"哒" + 1 次振动
- 快速甩 10 档 → 滚动过程不响(更接近 iOS 系统 picker)→ 停下 → 1 声"哒" + 1 次振动
- 用户感知:干净利落、毫无延迟
想问你三个决策点
在动手前先对齐:
问题 1:是否完全采纳"只管最后一步"?
- A. 是,全面回退(推荐 ✅)
- B. 还想保留逐档音效作为可选模式(加个
tickStrategy: 'final' | 'inertia' | 'realtime')
问题 2:pick-tick 事件保留吗?
- A. 保留,但组件内部不调用(业务想自己做时用)
- B. 直接删掉,连同
triggerColumnTick、enable-tick-*属性等都简化
问题 3:振动强度
- A.
type: 'medium'(更明显的"咔",弥补只响一声) - B.
type: 'light'(保持现在的轻盈)
我的推荐组合是 1A + 2A + 3A:完全回退到落点响一声 + 保留 pick-tick 钩子兜底 + 振动加重。
你拍板,我立刻动手。
逐档触发
2026-06-17
✅ 改动汇总
| # | 文件 | 变更 |
|---|---|---|
| 1 | picker-item.vue | created/onTouchStart 维护 _lastTickIndex;onTouchMove 跨档时调用 triggerColumnTick;onTouchEnd 同步该索引 |
| 2 | picker.vue | 新增 triggerColumnTick 方法 → $emit('pick-tick', { column, index }) |
| 3 | picker/props.ts | 新增 onPickTick 声明 |
| 4 | picker/type.ts | 新增 onPickTick 类型 |
| 5 | picker/README.md | 文档新增 pick-tick 行 |
| 6 | tdesign-api/db/TDesign.db | 插入一条 Event 记录(id=1781676506),避免下次脚本回写覆盖 |
📱 业务侧使用示例
<t-picker @pick-tick="onPickTick" @change="onChange">
<t-picker-item :options="years" />
</t-picker>// 预创建多个 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
},
}⚠️ 注意事项
- 触发频率:
pick-tick在touchmove中检测到跨档时立刻触发,正常匀速滑动每越过一档触发一次;惯性飞滑时touchend后用的是 transform 动画,惯性段不会逐档触发(因为没有 touchmove 事件)。如需"惯性飞滑也哒哒哒响",需要在onTouchEnd里另起一个定时器按预测落点节奏播音效,复杂度高,本次未做。 - 音频实例池:用单个
audio实例快速.play()在 H5/小程序上会出现"播不全/丢音",所以推荐 4 个实例轮播。 - RELATION_MAP 调用:使用了
?.triggerColumnTick?.()(可选链),即使父 picker 是旧版本不存在该方法也不会报错,向后兼容。
🔄 别忘了
- 原生小程序版
packages/components/picker、picker-item还没改(你的需求只针对 uniapp 版还是要两个版本同步?)。 - 上面 SQLite 我只插了 UniApp(platform_framework=128)。如果要小程序版也支持,需把掩码改成
192(128+64)或单独再插一条 64 的,并改原生小程序版的.ts实现。
Picker Tick
2026-06-17
✅ 方案 B+ 实施完成
改动汇总
| # | 文件 | 变更 |
|---|---|---|
| 1 | picker/tick-sound.ts | 新建,导出 30ms 内置"咔哒"声 base64 data URI(1824 字符 ≈ 1.4KB) |
| 2 | picker/props.ts | 新增 3 个 props(按字典序插入) |
| 3 | picker/type.ts | 新增 3 个属性类型 |
| 4 | picker/picker.vue | 引入音效;triggerColumnTick 内置 playTickFeedback;4 实例懒加载音频池轮询播放;振动 30ms 节流;beforeUnmount 销毁释放;emits 补 pick-tick |
| 5 | picker/README.md | Props 表新增 3 行 |
| 6 | tdesign-api/db/TDesign.db | 插入 3 条 Props(id=600/601/602,platform_framework=128) |
业务侧使用(一行属性搞定 🎉)
<!-- 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 事件 | 业务仍可监听该事件做额外定制(埋点、自定义音效等) |
⚠️ 已知限制
- 微信小程序的 data URI 限制:微信
wx.createInnerAudioContext().src不支持data:audio/wav;base64,...,必须传tick-sound-src静态资源路径才能听到声音;振动则不受影响。 - 惯性飞滑无音效:
pick-tick来自touchmove检测跨档,惯性段是 transform 动画,没有 touchmove 事件,所以惯性飞滑期间不响——这点与 iOS 原生略有差异。如需做到 iOS 那种惯性段也响,需要在onTouchEnd里另起预测定时器,复杂度高,本次没做。 - 原生小程序版本未同步:
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。
// 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() 会自动重置;只增大池容量:
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 系统:
// 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.vueonTouchEnd里补惯性段 tick +beforeUnmount清理定时器
或者你想要"豪华版",三个修复一起上?
[tabbar] 为什么 normal 下已经有安全区了呢,这个跟 round 不一样吗
2026-06-17
好问题!这正是我之前没解释清楚的关键点。我从 CSS 渲染机制角度详细说一下两者的本质差异:
看代码本质区别
来自 tab-bar.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 形态是圆角胶囊+悬浮阴影的设计:
&--round {
margin-left: 32rpx;
margin-right: 32rpx;
border-radius: 999px; // 完整胶囊圆角
box-shadow: @tab-bar-round-shadow;
}如果 round 用 padding-bottom: env(...),会导致:
- 胶囊变成上窄下宽的奇怪形状(padding 撑高内容区)
- 圆角失效(
border-radius: 999px在矩形上才是完美胶囊) - 阴影错位
所以 round 必须用 bottom 偏移——让胶囊保持原本形状,整体向上移开 home indicator。
那为什么 issue 里 normal 形态也报 bug?
理论上 normal 形态 getRect.height 已含安全区,不应该有 bug。
但实际有几种情况会让 normal 形态也翻车:
- 测量时机过早:
mounted→nextTick→getBoundingClientRect,部分 iOS 真机此时env(safe-area-inset-bottom)还没解析完,padding 还是 0,测出来的 height 不含安全区 - iOS WebView 在某些版本返回的 boundingClientRect 不包含 env() 计算出的 padding(已知 bug,社区有反馈)
- 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 内容里)。整体写得不算差,但作为对外官方文档,口语化、指代不清、措辞过随意的地方有一些。下面分两块:
- 我的诊断(哪里不专业 / 不够清晰)
- 给出可直接落地的修订版
一、问题诊断
| 位置 | 原文 | 问题 |
|---|---|---|
| 写在前面 | "可以查看这篇文章" | "这篇文章"过于口语化,建议明确"实现原理介绍" |
| 外部样式类 | "在 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 是否真的存在,避免我在修文档时把不存在的组件保留下来:
好,drawer 和 color-picker 都存在,原列表无需删除。
那我直接落地修订。下面是「先给你看修订对照,再问你是否落盘」的方式——但你这次问题是"看看有没有可以优化的语句,不专业的语句可以优化的",更倾向于建议而不是直接改。所以我不动文件,先把建议改写贴出来,等你确认后再修改。
二、修订建议(按段落给出)
① "写在前面"
- 原:TDesign UniApp 的实现原理,可以查看这篇文章。
- 改:TDesign UniApp 的实现原理,可参考原理介绍。
② "外部样式类"
原:
tdesign-miniprogram中的externalClasses,在tdesign-uniapp中被转成了props。传参方式不变,都是
t-class="xxx-class"。使用的时候要在样式文件中增加:deep标记,否则会因为scoped而失败,如果是非页面级别组件,需要增加options: { styleIsolation: 'shared' }。改:
tdesign-miniprogram中的externalClasses在tdesign-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的类型。解决方案:
- 注释掉
src/env.d.ts文件中vue的声明 tsconfig.json中配置"moduleResolution": "bundler"
...
另外,它这个脚手架太老了,自己的
tsconfig.json还飘红,升级下@vue/tsconfig可解决。这里有一个开箱即用的 uniapp Vue3 脚手架项目,支持自动导入、类型提示等,你可以打开看看。
- 注释掉
改:
uniapp 官方脚手架的默认配置中,
src/env.d.ts对vue模块的类型声明不完整,未导出 slot 相关类型,会导致使用组件时缺失类型提示。解决方案:
- 注释掉
src/env.d.ts中declare module 'vue'部分; - 在
tsconfig.json中将compilerOptions.moduleResolution设置为"bundler"。
...
此外,官方脚手架自带的
tsconfig.json在新版 TypeScript 下会出现类型报错,可通过升级@vue/tsconfig至最新版本解决。推荐使用我们提供的开箱即用模板:tdesign-uniapp-starter,已内置自动导入、类型提示等配置。
- 注释掉
⑥ "visible 受控"(基本可保留,仅微调)
- 原:"下面几个组件在关闭时,需要父组件中设置
visible为false,否则无法再次开启。也就是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.vue 中 updateNav 方法目前调用了 this.getRelationNodes('./swiper-nav'):
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.js 的 bindRelation)。
而且这里其实逻辑本身有问题:
- 原小程序
updateNav的目的是把current/total/direction/paginationPosition同步给<t-swiper-nav>子组件。 - uniapp 版本里
<t-swiper-nav>已通过 props 双向绑定 (:current="navCurrent"、:total="list.length"、:direction、:pagination-position),本身就是响应式的,根本不需要再手动 setData。 - 当前的
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 完成了响应式绑定:
<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
- UniApp 从无到有,全平台适配 — 主导 tdesign-uniapp 从 0.1.0 → 0.9.0 的 多个 个版本发布,支持 Vue2/Vue3 + H5 + 微信小程序等多端
- Table 组件开发 — 新增 Table 组件
- 主题系统 — 实现 theme-light 支持 及 uniapp 主题样式
- Chat 组件库 — 发布 tdesign-uniapp-chat 0.1.0 ~ 0.2.3,包含 chat-list、chat-thinking 等组件
- 多项 Bug 修复 — stepper、upload、search、dialog、sidebar 等组件问题修复
tdesign-mobile-vue
- Table 组件增强(scroll-to-bottom、footerSummary、fixedRows)
- Dialog 异步 onConfirm 支持 (#2166)
- Message 组件 single/gap 属性 (#1756)
- Slider 垂直模式 (#1745)
- Form string pattern 支持 (#1972)
- 修复 cascader/tabs/radio/pull-down-refresh 等多组件问题
- 发布 v1.9.1、v1.10.1、v1.11.0-beta 共 3 个版本
tdesign-mobile-react
- DateTimePicker 新组件 (#672)
- Table 组件 fixedRows + column.fixed 支持 (#662)
- Calendar switchMode 支持 (#663)
- Tabs 自动滚动到激活 tab (#693)
- Popup duration 属性 (#691)
- 修复 sticky/pull-down-refresh/guide/notice-bar 等组件问题
- 补充 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 行定义了:
transition: all 0.3s;这个 transition: all 是一切问题的根源。它意味着 该元素上所有可动画 CSS 属性的变化都会有 0.3 秒的过渡动画,包括 color、background-color、border-color、opacity 等。
触发链路
当 disabled 属性发生变化时,触发路径如下:
具体来说:
Vue 组件层:
button.vue中watch监听disabled变化 → 调用setClass()→ 动态添加/移除t-button--disabled类名CSS 层:
t-button--disabled类名会改变以下属性(以 primary 主题为例):color: 正常色 →@button-primary-disabled-colorbackground-color: 正常色 →@button-primary-disabled-bg(@brand-color-disabled)border-color: 正常色 →@button-primary-disabled-border-color
过渡效果:由于
transition: all 0.3s的存在,上述 color/background/border 的变化会在 0.3 秒内平滑过渡,产生渐变效果
为什么说是"突变"
Issue #4291 描述的应该是:这个渐变效果本身就不符合预期。对于 disabled 状态的切换,用户期望的是瞬间变为禁用态外观,而不是看到颜色慢慢渐变过去。这里所谓的"突变"可能是指:
- 在某些场景下(比如点击后立即 disable),用户能明显感知到颜色从正常态"慢慢褪色"到禁用态,体验不佳
transition: all过于粗暴,它不应该影响 disabled 状态切换,本意只是为了 hover 按压态的过渡效果
解决方案
transition: all 0.3s 应该限定作用范围,而不是对所有属性生效。有两种方式:
方案一:精确指定 transition 属性(推荐)
// 只对 hover 按压态需要的属性做过渡
transition: background-color 0.3s, border-color 0.3s;方案二:在 disabled 类上覆盖 transition
&.@{button}--disabled {
transition: none; // 禁用态不需要过渡动画
// ...existing styles...
}方案三:结合两者
.@{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 部署后找不到。
+ <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 包发布后无法使用
当前所有数据来源都是 读取本地文件系统:
// 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-list 用 fs.readdir 读目录、get-component-docs 用 readLocalFile 读 Markdown、get-component-source 用 readLocalFile 读 Vue 文件。
但看 package.json 的 files 字段只包含 ["dist", "README.md"],文档和源码根本不会被打包进 npm 发布物里。也就是说:
- ✅ 本地开发时可用(因为文档和源码就在旁边的目录里)
- ❌ 通过 NPM 安装后完全不可用(
dist/目录里不包含文档和源码,读取路径全部失效)
建议方案
有几种修复思路,你看想用哪种:
方案 A:构建时内嵌(推荐)
在 rollup 构建阶段,把文档和源码的内容预处理后内嵌到 JS 产物中(比如生成一个 data.json),打包进 dist/。这样 NPM 发布后也能用,同时也顺带解决了中间文档的问题——可以在构建时做精简。
方案 B:远程获取
把文档和源码部署到 CDN 或 API 服务,MCP 通过 HTTP 请求获取,而不是读取本地文件。
方案 C:把文档也打包进 files
修改 package.json 的 files 字段,把 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.gtim…
2026-02-14
- 组件列表 https://tdesign.gtimg.com/mcp/tdesign-vue-next/index.json
- dom 结构 https://tdesign.gtimg.com/mcp/tdesign-web-dom/avatar.html
- 文档 https://tdesign.gtimg.com/mcp/tdesign-vue-next/button/api.md
- 示例 https://tdesign.gtimg.com/mcp/tdesign-react/button/Demo.tsx,https://tdesign.gtimg.com/mcp/tdesign-vue-next/button/Demo.vue
- 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.js | es/index.mjs |
|---|---|---|
| 打包方式 | 单文件打包(所有代码打包在一起) | 保留源码结构(多文件) |
| 文件大小 | 较大(包含所有依赖) | 较小(入口文件) |
| 依赖处理 | 内部依赖已打包 | 依赖其他 .mjs 文件 |
| 加载方式 | 加载一个文件即可 | 需要加载多个文件 |
| Tree Shaking | 支持(但已打包) | 支持(更细粒度) |
潜在影响
优点:
- 🌲 更好的 Tree Shaking:打包工具可以更精确地按需引入
- 📦 更小的初始体积:只加载入口文件,其他按需加载
- 🔧 更灵活的优化:打包工具可以对每个模块单独优化
缺点:
- ⚠️ 依赖关系复杂:需要确保
es/目录下的所有文件都正确发布 - ⚠️ 兼容性问题:某些老旧的打包工具可能不支持
.mjs扩展名 - ⚠️ 文件数量多:需要发布整个
es/目录
📊 产物对比表
2026-02-11
📊 产物对比表
| 产物 | 格式 | 目录结构 | 文件数量 | 使用场景 |
|---|---|---|---|---|
| lib/*.js | CommonJS | 保留 | 多文件 | Node.js 环境 |
| lib/index.esm.js | ES Module | 单文件 | 1 个 | 现代打包工具(单文件) |
| es/*.mjs | ES Module | 保留 | 多文件 | 现代打包工具(按需引入) |
之前运行时的引入方式是 ,包体积大小:
2026-02-11
之前运行时的引入方式是 import { xxx } from 't-comm/lib/xx/xx',包体积大小:

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

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

为什么 没问题, 在浏览器中就有问题,是 做了什么吗
2026-02-11
为什么 import * as fs from 'fs' 没问题,import { execSync, spawnSync } from 'child_process'; 在浏览器中就有问题,是 vite 做了什么吗
核心原因如下。
fs 和 child_process 都是 Node.js 内置模块,它们在浏览器环境中都是不存在的。但为什么表现不同呢?这主要是因为 Vite(以及底层的 Rollup/esbuild)对不同模块的处理策略不同。
fs模块的处理
当你在浏览器代码中 import * as fs from 'fs' 时:
- Vite 会自动将
fs模块"空化"(shimmed/polyfilled) - 对于
fs这类常见的 Node.js 模块,Vite/Rollup 默认会提供一个空的 polyfill - 导入不会报错,但
fs对象会是一个空对象{},调用任何方法都会失败
你可以验证:
import * as fs from 'fs';
console.log(fs); // 在浏览器中会输出 {} 或 undefinedchild_process模块的处理
而 child_process 模块:
- Vite 默认不会为它提供 polyfill
- 当你尝试导入时,打包工具找不到这个模块,就会直接报错
- 这是因为
child_process涉及系统进程操作,根本无法在浏览器中模拟
- Vite 的具体处理机制
Vite 使用了 @rollup/plugin-node-resolve 插件,它有一个 browser 字段处理逻辑:
// Vite 内部大致的处理逻辑
{
// 这些模块会被替换为空模块
"fs": false, // 被 shimmed 为空
"path": "path-browserify", // 有 polyfill
"child_process": undefined // 没有处理 → 报错
}- 为什么这样设计?
| 模块 | 浏览器 Polyfill | 原因 |
|---|---|---|
fs | 空对象 {} | 某些库可能条件引入,空化可以避免打包失败 |
path | path-browserify | 路径操作可以在浏览器模拟 |
child_process | ❌ 无 | 进程操作完全无法模拟,直接报错更清晰 |
解决方案
如果你的代码中确实需要在浏览器中引用这些模块(比如共享代码),可以在 vite.config.ts 中配置:
export default defineConfig({
resolve: {
alias: {
// 将 child_process 也替换为空模块
'child_process': 'rollup-plugin-node-polyfills/polyfills/empty'
}
},
// 或者使用 define 将其标记为外部模块
define: {
'process.env': {}
}
})或者使用条件导入:
// 只在 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 时,包管理器会自动:
- 检查本地是否有 create-uni 包,没有则临时安装
- 执行 create-uni 包中的可执行脚本(一般在 package.json 的 bin 字段定义)
- 脚本执行脚手架的核心逻辑(拉取模板、初始化项目、交互配置等)
图片笔记
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 2 | Vue 3 |
|---|---|---|
v-model:visible="visible" | ❌ 不支持 | ✅ 支持 |
:visible.sync="visible" | ✅ 支持 | ❌ 已移除 |
:visible="visible" @update:visible="..." | ✅ 支持 | ✅ 支持 |
使用 :visible + @update:visible 的手动绑定方式可以同时兼容 Vue 2 和 Vue 3。
javascript 代码片段
2026-02-03
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 不是一个有效的构造函数。
解决方案:将 null 从 type 数组中移除,只保留有效的类型构造函数。如果需要允许 null 值,可以通过 default: null 和 validator 来处理。
已修复!将第 166 行的:
type: [null, ...newType],修改为:
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
修复说明
- 移除了
"type": "module"
当同时提供 ESM 和 CommonJS 两种格式时,不应设置 type: module,而应通过 exports 字段来声明不同格式的入口。
- 修正了入口文件路径
main:dist/index.js→dist/index.cjs(CommonJS 入口)module:dist/index.mjs→dist/index.js(ESM 入口,实际文件名)
- 添加了
exports字段
"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
- 添加了
sideEffects字段
"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
pip3 install lizard -i http://pypi.douban.com/simple --trusted-host pypi.douban.comlizard -x "**/node_modules/*"python2 装这个版本
pip install lizard==1.17.10pandoraShowEntrance
2026-01-29
pandoraShowEntrance
css 尽量复用小程序端的
2026-01-29
css 尽量复用小程序端的
- 小程序端和uniapp端样式部分差异小,相同部分远大于不同部分
- css 难diff,一行一行的太分散,如果不复用的话,精确同步太费时间
既然要复用CSS
- 其衍生出的文档中的 CSS 变量部分也要复用,或者生成变量的脚本复用
- 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
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-plugin 和 dist/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个)🎯 核心文件详解
- HookCenter.js - 语法钩子注册中心
作用:管理所有 Markdown 语法钩子的注册、分类和优先级
核心功能:
| 方法 | 说明 |
|---|---|
registerInternalHooks() | 注册系统内置的语法钩子 |
registerCustomHooks() | 注册用户自定义的语法钩子 |
register() | 实际注册一个钩子实例 |
getHookList() | 获取所有钩子(按类型分组) |
关键逻辑:
// 钩子分为两类
this.hookList = {
sentence: [], // 行内语法钩子(如加粗、斜体)
paragraph: [], // 块级语法钩子(如标题、代码块)
};自定义钩子支持:
- 可以指定
before/after插入位置 - 可以设置
force: true覆盖同名内置钩子
- HooksConfig.js - 默认钩子配置
作用:定义所有内置语法钩子的加载顺序
执行顺序规则:
beforeMakeHtml:按数组顺序正序执行makeHtml:按数组顺序正序执行afterMakeHtml:按数组顺序逆序执行
钩子加载顺序:
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, // 连续空格
];- SyntaxBase.js - 行内语法基类
作用:所有行内语法钩子的基类(如加粗、斜体、链接)
生命周期方法:
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('') };
}
}类型定义:
export const HOOKS_TYPE_LIST = {
SEN: 'sentence', // 行内语法
PAR: 'paragraph', // 块级语法
DEFAULT: 'sentence',
};- ParagraphBase.js - 块级语法基类
作用:所有块级语法钩子的基类(如标题、代码块、表格)
与 SyntaxBase 的区别:
| 特性 | SyntaxBase | ParagraphBase |
|---|---|---|
| 类型 | sentence | paragraph |
| 缓存机制 | ❌ | ✅ |
| 换行处理 | ❌ | ✅ |
| 行号计算 | ❌ | ✅ |
缓存机制:
// 缓存用于提升性能,避免重复渲染
pushCache(str, sign, lineCount) // 存入缓存
popCache(sign) // 取出缓存
restoreCache(html) // 还原所有缓存
checkCache(wholeMatch, ...) // 检查是否命中缓存缓存键格式:
~~C${cacheCounter}I${sign}_L${lineCount}$
例如:~~C0Iabc123_L5$换行处理:
// 经典模式 vs 现代模式
this.classicBr = true; // 一个换行被忽略,两个换行分段
this.classicBr = false; // 一个换行变<br>,两个换行分段- SentenceBase.js - 句子级基类(已弃用)
作用:早期版本的钩子基类,现已基本弃用
class HookBase {
getType() {
const typeList = { 1: 'sentence', 2: 'paragraph', 3: 'page' };
return typeList[this.HOOKTYPE] || 'sentence';
}
}📂 hooks/ 子目录 - 具体语法实现
块级语法钩子(22个)
| 文件 | 钩子名 | 语法示例 | 说明 |
|---|---|---|---|
| Header.js | header | # 标题 | 支持 ATX(#)和 Setext(===)两种风格 |
| CodeBlock.js | codeBlock | ```js | 支持语法高亮、行号、复制、展开、自定义渲染器 |
| Table.js | table | |a|b| | 支持对齐、图表渲染(ECharts) |
| List.js | list | - item | 支持有序、无序、任务列表、多种样式 |
| Blockquote.js | blockquote | > 引用 | 引用块 |
| MathBlock.js | mathBlock | $$ ... $$ | 块级数学公式(MathJax/KaTeX) |
| Footnote.js | footnote | [^1] | 脚注 |
| Toc.js | toc | [[toc]] | 自动生成目录 |
| Hr.js | hr | --- | 水平分割线 |
| Br.js | br | 换行 | 换行处理 |
| HtmlBlock.js | htmlBlock | <div> | HTML 块级元素 |
| FrontMatter.js | frontMatter | ---\nyaml\n--- | YAML 元数据 |
| Panel.js | panel | 自定义面板 | 信息/警告/错误面板 |
| Detail.js | detail | <details> | 可折叠内容 |
| Paragraph.js | paragraph | 普通文本 | 普通段落(兜底) |
| CommentReference.js | commentReference | [ref]: url | 全局引用定义 |
| Transfer.js | transfer | \* | 转义字符处理 |
| AiFlowAutoClose.js | aiFlowAutoClose | - | AI 流式输出自动闭合 |
| InlineCode.js | inlineCode | `code` | 行内代码(在块级处理) |
| InlineMath.js | inlineMath | $x^2$ | 行内公式(在块级处理) |
行内语法钩子(15个)
| 文件 | 钩子名 | 语法示例 | 说明 |
|---|---|---|---|
| Emphasis.js | fontEmphasis | **粗体** *斜体* | 支持 * 和 _ 两种符号 |
| Image.js | image | !alt | 支持扩展属性、视频/音频 |
| Link.js | link | text | 支持 target 属性 |
| AutoLink.js | autoLink | https://... | 自动识别 URL |
| Strikethrough.js | strikethrough | ~~删除~~ | 删除线 |
| Underline.js | underline | - | 下划线 |
| HighLight.js | highLight | ==高亮== | 文字高亮 |
| Color.js | color | - | 文字颜色 |
| BackgroundColor.js | backgroundColor | - | 背景颜色 |
| Size.js | size | - | 字体大小 |
| Sub.js | sub | H~2~O | 下标 |
| Sup.js | sup | X^2^ | 上标 |
| Ruby.js | ruby | - | 注音(ruby 标签) |
| Emoji.js | emoji | :smile: | 表情符号 |
| Suggester.js | suggester | @user | @ 提及/智能建议 |
| Space.js | space | 多个空格 | 连续空格保留 |
| SuggestList.js | - | - | Suggester 的辅助模块 |
| Emoji.config.js | - | - | Emoji 配置数据 |
🔄 语法解析流程
🛠️ 如何创建自定义语法钩子
- 行内语法示例
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,
};
}
}- 块级语法示例
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 的语法解析核心:
- HookCenter - 统一管理所有语法钩子的注册和调度
- HooksConfig - 定义语法处理的优先级顺序
- SyntaxBase - 行内语法的基类(轻量、无缓存)
- ParagraphBase - 块级语法的基类(支持缓存、行号计算)
- hooks/ - 37 个具体语法实现
设计亮点:
- 🔌 插件化:支持自定义语法扩展
- ⚡ 高性能:块级语法支持缓存
- 🎯 优先级:通过配置数组控制处理顺序
- 🔄 生命周期:
beforeMakeHtml→makeHtml→afterMakeHtml
mermaid 代码片段
2026-01-28
方法永远可以做到不私密,如果存在私密的方法,那么可以拆分…
2026-01-28
方法永远可以做到不私密,如果存在私密的方法,那么可以拆分出不私密的方法和私密的变量。
真正私密的永远是配置信息、密钥等常量,所以上面的方式永远有效。
plugin-light-const 的定位:
2026-01-28
plugin-light-const 的定位:
- 放配置信息、常量定义,比如 getCdnList
- 有点私密,不方便放 t-comm 里
- 如果是需要运行时和编译时都需要的函数,放到 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 包内容
- 进入工程,
cd packages/tdesign-uniapp/example - 去掉
vite.config.ts中alias的配置 - 装包,如
pnpm i https://pkg.pr.new/Tencent/tdesign-miniprogram/tdesign-uniapp@4201 - 执行
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://gamelife-1251917893.igcdn.cn/hpmatch/hpmatch_F6dFij4NT8R5.jpg
- 正确的:https://gamelife-1251917893.igcdn.cn/hpmatch/hpmatch_F6dFij4NT8R5.jpg?q-sign-algorithm=sha1&q-ak=xx&q-sign-time=xx&q-key-time=xx&q-header-list=host&q-url-param-list=&q-signature=xx
- https://github.com/Tencent…
2026-01-26
- https://github.com/Tencent/tdesign-miniprogram/pull/4112/changes
- https://github.com/Tencent/tdesign-miniprogram/pull/4124/changes
这两个还要再看下
td-mini 同步 td-uniapp 的步骤:
2026-01-26
td-mini 同步 td-uniapp 的步骤:
- 可选,在 td-mini 大仓下进行 build 脚本的改造,去掉
jsmin/jsonmin/wxmlmin的使用 - 执行
npm run build(或者npm run build -- --ignore-terser),生成_example目录 - 复制
_example目录到mini-to-uni工程下,进行覆盖 - 可选,删除之前的
_example_uni mini-to-uni工程下执行node ./bin/wtu -i ./_example进行 uniapp 组件生成- 手动 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 类名,就是白底黑色,否则就是透明底默认颜色。
.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。
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 更易读。