Repository: Tencent/tdesign-mobile-vue
Pull request #364 (2022-09-21 03:19:35)

fix(Slider): complete the label attribute && add test unit
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [x] 日常 bug 修复
- [x] 测试用例


### 🔗 相关 Issue
#362

### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->

- fix(Slider): 修复 `label` 属性无效的问题
- feat(Slider): 新增单元测试



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e78aff15..64e51add 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,110 +5,128 @@ toc: false
 spline: explain
 ---
 
-## 🌈 0.10.6 `2022-09-20` 
-### 🚀 Features
-- `Drawer`: 新增单元测试 @anlyyao ([#311](https://github.com/Tencent/tdesign-mobile-vue/pull/311))
-- `Popup`: 新增单元测试 @anlyyao ([#310](https://github.com/Tencent/tdesign-mobile-vue/pull/310))
-- `Stepper`: 新增单元测试 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
-- `Overlay`: 新增单元测试 @HelKyle ([#336](https://github.com/Tencent/tdesign-mobile-vue/pull/336))
-- `BackTop`: 新增单元测试 @HelKyle ([#335](https://github.com/Tencent/tdesign-mobile-vue/pull/335))
-- `Toast`: 新增单元测试 @HelKyle ([#334](https://github.com/Tencent/tdesign-mobile-vue/pull/334))
-- `ActionSheet`: 新增单元测试 @HelKyle ([#319](https://github.com/Tencent/tdesign-mobile-vue/pull/319))
-- `Progress`: 新增单元测试 @HelKyle ([#337](https://github.com/Tencent/tdesign-mobile-vue/pull/337))
-- `ImageViewer`: 新增单元测试 @palmcivet ([#332](https://github.com/Tencent/tdesign-mobile-vue/pull/332))
-- `TabBar`: 新增单元测试 @HelKyle ([#314](https://github.com/Tencent/tdesign-mobile-vue/pull/314))
-- `Dialog`: 新增单元测试 @anlyyao ([#361](https://github.com/Tencent/tdesign-mobile-vue/pull/361))
-### 🐞 Bug Fixes
-- `Stepper`: 修复输入非 `number` 字符时，出现 `NaN` 问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
-- `Stepper`: 修复输入值能超出 `max` 的问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
-- `Stepper`: 修复 `value` 值小于 `min` 或超出 `max` 时，不触发 `overlimit` 的问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
-- `Collapse`: 修复 `headerRightContent` 插槽与文档描述不一致的问题 @isanxia ([#330](https://github.com/Tencent/tdesign-mobile-vue/pull/330))
-### 🚧 Others
+## 🌈 0.11.0 `2022-09-26` 
+### 🚀 Features
+
+- `Calendar`: 新增日历组件 @LadyChatterleyLover ([#325](https://github.com/Tencent/tdesign-mobile-vue/pull/325))
+- `Calendar`: 视觉升级，功能增强 @LeeJim ([#366](https://github.com/Tencent/tdesign-mobile-vue/pull/366))
+
+### 🐞 Bug Fixes
+- `CountDown`: 修复 milisecond 配置无效的问题 @TingShine ([#313](https://github.com/Tencent/tdesign-mobile-vue/pull/313))
+- `Upload`: 修复上传失败后，再次上传会产生报错的问题 @palmcivet ([#318](https://github.com/Tencent/tdesign-mobile-vue/pull/318))
+
+### 🚧 Others
+- `Collapse`: 新增单元测试 @isanxia ([#331](https://github.com/Tencent/tdesign-mobile-vue/pull/331))
+- `Loading`: 新增单元测试 @zhangpaopao0609 ([#320](https://github.com/Tencent/tdesign-mobile-vue/pull/320))
+- `PullDownRefresh`: 新增单元测试 @palmcivet ([#356](https://github.com/Tencent/tdesign-mobile-vue/pull/356))
+- `CountDown`: 新增单元测试 @TingShine ([#313](https://github.com/Tencent/tdesign-mobile-vue/pull/313))
+- `Message`: 新增单元测试 @zhangpaopao0609 ([#321](https://github.com/Tencent/tdesign-mobile-vue/pull/321))
+- `Upload`: 新增单元测试 @palmcivet ([#318](https://github.com/Tencent/tdesign-mobile-vue/pull/318))
+
+## 🌈 0.10.6 `2022-09-20` 
+### 🚀 Features
+- `Drawer`: 新增单元测试 @anlyyao ([#311](https://github.com/Tencent/tdesign-mobile-vue/pull/311))
+- `Popup`: 新增单元测试 @anlyyao ([#310](https://github.com/Tencent/tdesign-mobile-vue/pull/310))
+- `Stepper`: 新增单元测试 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Overlay`: 新增单元测试 @HelKyle ([#336](https://github.com/Tencent/tdesign-mobile-vue/pull/336))
+- `BackTop`: 新增单元测试 @HelKyle ([#335](https://github.com/Tencent/tdesign-mobile-vue/pull/335))
+- `Toast`: 新增单元测试 @HelKyle ([#334](https://github.com/Tencent/tdesign-mobile-vue/pull/334))
+- `ActionSheet`: 新增单元测试 @HelKyle ([#319](https://github.com/Tencent/tdesign-mobile-vue/pull/319))
+- `Progress`: 新增单元测试 @HelKyle ([#337](https://github.com/Tencent/tdesign-mobile-vue/pull/337))
+- `ImageViewer`: 新增单元测试 @palmcivet ([#332](https://github.com/Tencent/tdesign-mobile-vue/pull/332))
+- `TabBar`: 新增单元测试 @HelKyle ([#314](https://github.com/Tencent/tdesign-mobile-vue/pull/314))
+- `Dialog`: 新增单元测试 @anlyyao ([#361](https://github.com/Tencent/tdesign-mobile-vue/pull/361))
+### 🐞 Bug Fixes
+- `Stepper`: 修复输入非 `number` 字符时，出现 `NaN` 问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Stepper`: 修复输入值能超出 `max` 的问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Stepper`: 修复 `value` 值小于 `min` 或超出 `max` 时，不触发 `overlimit` 的问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Collapse`: 修复 `headerRightContent` 插槽与文档描述不一致的问题 @isanxia ([#330](https://github.com/Tencent/tdesign-mobile-vue/pull/330))
+### 🚧 Others
 - `官网`: 更新浏览器兼容性说明文档 @anlyyao ([#349](https://github.com/Tencent/tdesign-mobile-vue/pull/349))
 
-## 🌈 0.10.5 `2022-09-06` 
-### 🚀 Features
-- `Popup`: 新增 `customStyle` 和 `overlayProps` 属性 @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
-- `Popup`: 更新示例代码 @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
-- `Drawer`: 更新示例代码 @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
-- `Switch`: 更新示例代码 @anlyyao ([#307](https://github.com/Tencent/tdesign-mobile-vue/pull/307))
-- `Switch`: 新增单元测试 @anlyyao ([#307](https://github.com/Tencent/tdesign-mobile-vue/pull/307))
-### 🐞 Bug Fixes
-- `Popup`: `closeOverlayClick`属性更名为 `closeOnOverlayClick` @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
-- `Drawer`: `closeOverlayClick`属性更名为 `closeOnOverlayClick` @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
-- `Dialog`: 修复 content 变更没有响应的问题 @TingShine ([#306](https://github.com/Tencent/tdesign-mobile-vue/pull/306))
-- `ActionSheet`: 修复type=grid时没有触发 onSelected的问题 @HelKyle ([#317](https://github.com/Tencent/tdesign-mobile-vue/pull/317))
-- `DateTimePicker`: 修复组件修改model无法双向绑定的问题；修复组件默认value为空时，无法正常滑动的问题；优化List组件demo展示；修复Upload组件无法正常上传文件的问题 @Dengzygx ([#344](https://github.com/Tencent/tdesign-mobile-vue/pull/344))
--  `List`: 优化List组件demo展示；修复Upload组件无法正常上传文件的问题 @Dengzygx ([#344](https://github.com/Tencent/tdesign-mobile-vue/pull/344))
-- `Upload`: 修复Upload组件无法正常上传文件的问题 @Dengzygx ([#344](https://github.com/Tencent/tdesign-mobile-vue/pull/344))
-### 🚧 Others
-- style: update _common @anlyyao ([#308](https://github.com/Tencent/tdesign-mobile-vue/pull/308))
+## 🌈 0.10.5 `2022-09-06` 
+### 🚀 Features
+- `Popup`: 新增 `customStyle` 和 `overlayProps` 属性 @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
+- `Popup`: 更新示例代码 @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
+- `Drawer`: 更新示例代码 @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
+- `Switch`: 更新示例代码 @anlyyao ([#307](https://github.com/Tencent/tdesign-mobile-vue/pull/307))
+- `Switch`: 新增单元测试 @anlyyao ([#307](https://github.com/Tencent/tdesign-mobile-vue/pull/307))
+### 🐞 Bug Fixes
+- `Popup`: `closeOverlayClick`属性更名为 `closeOnOverlayClick` @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
+- `Drawer`: `closeOverlayClick`属性更名为 `closeOnOverlayClick` @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
+- `Dialog`: 修复 content 变更没有响应的问题 @TingShine ([#306](https://github.com/Tencent/tdesign-mobile-vue/pull/306))
+- `ActionSheet`: 修复type=grid时没有触发 onSelected的问题 @HelKyle ([#317](https://github.com/Tencent/tdesign-mobile-vue/pull/317))
+- `DateTimePicker`: 修复组件修改model无法双向绑定的问题；修复组件默认value为空时，无法正常滑动的问题；优化List组件demo展示；修复Upload组件无法正常上传文件的问题 @Dengzygx ([#344](https://github.com/Tencent/tdesign-mobile-vue/pull/344))
+-  `List`: 优化List组件demo展示；修复Upload组件无法正常上传文件的问题 @Dengzygx ([#344](https://github.com/Tencent/tdesign-mobile-vue/pull/344))
+- `Upload`: 修复Upload组件无法正常上传文件的问题 @Dengzygx ([#344](https://github.com/Tencent/tdesign-mobile-vue/pull/344))
+### 🚧 Others
+- style: update _common @anlyyao ([#308](https://github.com/Tencent/tdesign-mobile-vue/pull/308))
 - fix: enrich coverage badge type @anlyyao ([#315](https://github.com/Tencent/tdesign-mobile-vue/pull/315))
 
-## 🌈 0.10.4 `2022-08-22` 
-### 🚀 Features
-- `Search`: 内嵌 input 组件 type 调整为 search @VeryHandSomeBoy ([#293](https://github.com/Tencent/tdesign-mobile-vue/pull/293))
-- `Drawer`: 新增 closeOnOverlayClick 属性 @deadlyAce ([#287](https://github.com/Tencent/tdesign-mobile-vue/pull/287))
-- `Popup`: 新增 closeOnOverlayClick 属性 @deadlyAce ([#287](https://github.com/Tencent/tdesign-mobile-vue/pull/287))
-### 🐞 Bug Fixes
-- `Dialog`: 修复 `demo` 示例弹窗不显示问题 @zyqq ([#289](https://github.com/Tencent/tdesign-mobile-vue/pull/289))
-- `Slider`: 修复滑块滑到最左侧时，左侧文本被遮挡 @HelKyle ([#286](https://github.com/Tencent/tdesign-mobile-vue/pull/286))
-- `Search`: 修复传入默认值后需要点击才显示的问题 @VeryHandSomeBoy ([#292](https://github.com/Tencent/tdesign-mobile-vue/pull/292))
+## 🌈 0.10.4 `2022-08-22` 
+### 🚀 Features
+- `Search`: 内嵌 input 组件 type 调整为 search @VeryHandSomeBoy ([#293](https://github.com/Tencent/tdesign-mobile-vue/pull/293))
+- `Drawer`: 新增 closeOnOverlayClick 属性 @deadlyAce ([#287](https://github.com/Tencent/tdesign-mobile-vue/pull/287))
+- `Popup`: 新增 closeOnOverlayClick 属性 @deadlyAce ([#287](https://github.com/Tencent/tdesign-mobile-vue/pull/287))
+### 🐞 Bug Fixes
+- `Dialog`: 修复 `demo` 示例弹窗不显示问题 @zyqq ([#289](https://github.com/Tencent/tdesign-mobile-vue/pull/289))
+- `Slider`: 修复滑块滑到最左侧时，左侧文本被遮挡 @HelKyle ([#286](https://github.com/Tencent/tdesign-mobile-vue/pull/286))
+- `Search`: 修复传入默认值后需要点击才显示的问题 @VeryHandSomeBoy ([#292](https://github.com/Tencent/tdesign-mobile-vue/pull/292))
 - `Search`: 修复不触发 focus 事件的问题 @VeryHandSomeBoy ([#292](https://github.com/Tencent/tdesign-mobile-vue/pull/292))
 
-## 🌈 0.10.3 `2022-08-17` 
-### 🐞 Bug Fixes
-- `PulldownRefresh`: 修复下拉时偶尔失效的问题 @LeeJim ([#278](https://github.com/Tencent/tdesign-mobile-vue/pull/278))
-- `List`: 修复list组件滚动无法触发scroll的问题 @Dengzygx ([#283](https://github.com/Tencent/tdesign-mobile-vue/pull/283))
-### 🚧 Others
+## 🌈 0.10.3 `2022-08-17` 
+### 🐞 Bug Fixes
+- `PulldownRefresh`: 修复下拉时偶尔失效的问题 @LeeJim ([#278](https://github.com/Tencent/tdesign-mobile-vue/pull/278))
+- `List`: 修复list组件滚动无法触发scroll的问题 @Dengzygx ([#283](https://github.com/Tencent/tdesign-mobile-vue/pull/283))
+### 🚧 Others
 - test: update vitest config @anlyyao ([#272](https://github.com/Tencent/tdesign-mobile-vue/pull/272))
 
-## 🌈 0.10.2 `2022-08-15` 
-### 🚀 Features
-- `ImageViewer`: 新增 maxZoom、closeBtn、onClose 属性 @ruige24601 ([#259](https://github.com/Tencent/tdesign-mobile-vue/pull/259))
-- `ImageViewer`: 新增 close 事件 @ruige24601 ([#259](https://github.com/Tencent/tdesign-mobile-vue/pull/259))
-- `Swiper`: 新增 `paginationPosition` 属性 @anlyyao ([#262](https://github.com/Tencent/tdesign-mobile-vue/pull/262))
-### 🐞 Bug Fixes
-- `Textarea`: 修复onchange事件会执行两次的问题 @Dengzygx ([#270](https://github.com/Tencent/tdesign-mobile-vue/pull/270))
+## 🌈 0.10.2 `2022-08-15` 
+### 🚀 Features
+- `ImageViewer`: 新增 maxZoom、closeBtn、onClose 属性 @ruige24601 ([#259](https://github.com/Tencent/tdesign-mobile-vue/pull/259))
+- `ImageViewer`: 新增 close 事件 @ruige24601 ([#259](https://github.com/Tencent/tdesign-mobile-vue/pull/259))
+- `Swiper`: 新增 `paginationPosition` 属性 @anlyyao ([#262](https://github.com/Tencent/tdesign-mobile-vue/pull/262))
+### 🐞 Bug Fixes
+- `Textarea`: 修复onchange事件会执行两次的问题 @Dengzygx ([#270](https://github.com/Tencent/tdesign-mobile-vue/pull/270))
 - `Image`: 修复 src 变更没有重新渲染的问题 @VeryHandSomeBoy ([#264](https://github.com/Tencent/tdesign-mobile-vue/pull/264))
 
-## 🌈 0.10.1 `2022-08-01` 
-### 🐞 Bug Fixes
-- `search`: 修复失去焦点后输入内容被隐藏和清除按钮无法使用的问题 @VeryHandSomeBoy ([#257](https://github.com/Tencent/tdesign-mobile-vue/pull/257))
-- `Tabs`: 修复传入相同的值时仍触发 onChange 的问题 @VeryHandSomeBoy ([#254](https://github.com/Tencent/tdesign-mobile-vue/pull/254))
-- `Popup`: 修复使用 overlay 组件没传入 visible 导致告警的问题 @LeeJim ([#251](https://github.com/Tencent/tdesign-mobile-vue/pull/251))
+## 🌈 0.10.1 `2022-08-01` 
+### 🐞 Bug Fixes
+- `search`: 修复失去焦点后输入内容被隐藏和清除按钮无法使用的问题 @VeryHandSomeBoy ([#257](https://github.com/Tencent/tdesign-mobile-vue/pull/257))
+- `Tabs`: 修复传入相同的值时仍触发 onChange 的问题 @VeryHandSomeBoy ([#254](https://github.com/Tencent/tdesign-mobile-vue/pull/254))
+- `Popup`: 修复使用 overlay 组件没传入 visible 导致告警的问题 @LeeJim ([#251](https://github.com/Tencent/tdesign-mobile-vue/pull/251))
 - `Dialog`: 修复 slot 渲染逻辑 @LeeJim ([#258](https://github.com/Tencent/tdesign-mobile-vue/pull/258))
 
-## 🌈 0.10.0 `2022-07-25` 
-### 🚀 Features
-- `Skeleton`: 属性 theme 移除 avatar-text；新增 avatar、image、paragraph @anlyyao ([#239](https://github.com/Tencent/tdesign-mobile-vue/pull/239))
-- `Result`: 新增结果组件 @anlyyao ([#243](https://github.com/Tencent/tdesign-mobile-vue/pull/243))
-### 🐞 Bug Fixes
-- `Button`: 修复 loading 值改变时，控制台报错 @anlyyao ([#247](https://github.com/Tencent/tdesign-mobile-vue/pull/247))
-- `Skeleton`: 属性 rowCol 移除默认值 `[1, 1, 1, { width: 70% }]` @anlyyao ([#239](https://github.com/Tencent/tdesign-mobile-vue/pull/239))
-- `Toast`: 修复指令调用时不现实图标的问题 @douxpang ([#241](https://github.com/Tencent/tdesign-mobile-vue/pull/241))
-### 🚧 Others
-- 官网更新组件分类 @LeeJim ([#248](https://github.com/Tencent/tdesign-mobile-vue/pull/248))
+## 🌈 0.10.0 `2022-07-25` 
+### 🚀 Features
+- `Skeleton`: 属性 theme 移除 avatar-text；新增 avatar、image、paragraph @anlyyao ([#239](https://github.com/Tencent/tdesign-mobile-vue/pull/239))
+- `Result`: 新增结果组件 @anlyyao ([#243](https://github.com/Tencent/tdesign-mobile-vue/pull/243))
+### 🐞 Bug Fixes
+- `Button`: 修复 loading 值改变时，控制台报错 @anlyyao ([#247](https://github.com/Tencent/tdesign-mobile-vue/pull/247))
+- `Skeleton`: 属性 rowCol 移除默认值 `[1, 1, 1, { width: 70% }]` @anlyyao ([#239](https://github.com/Tencent/tdesign-mobile-vue/pull/239))
+- `Toast`: 修复指令调用时不现实图标的问题 @douxpang ([#241](https://github.com/Tencent/tdesign-mobile-vue/pull/241))
+### 🚧 Others
+- 官网更新组件分类 @LeeJim ([#248](https://github.com/Tencent/tdesign-mobile-vue/pull/248))
 - `Skeleton`: 更新示例代码 @anlyyao ([#239](https://github.com/Tencent/tdesign-mobile-vue/pull/239))
 
-## 🌈 0.9.2 `2022-07-18` 
-### 🐞 Bug Fixes
-- `DropdownMenu`: 修复树形状态下溢出时无法滚动的问题 @LeeJim ([#236](https://github.com/Tencent/tdesign-mobile-vue/pull/236))
-- `Sticky`: 修复 fixed 状态下丢失宽度的问题 @LeeJim ([#237](https://github.com/Tencent/tdesign-mobile-vue/pull/237))
+## 🌈 0.9.2 `2022-07-18` 
+### 🐞 Bug Fixes
+- `DropdownMenu`: 修复树形状态下溢出时无法滚动的问题 @LeeJim ([#236](https://github.com/Tencent/tdesign-mobile-vue/pull/236))
+- `Sticky`: 修复 fixed 状态下丢失宽度的问题 @LeeJim ([#237](https://github.com/Tencent/tdesign-mobile-vue/pull/237))
 - `Skeleton`: 修复 props 变化不重新渲染的问题 @VeryHandSomeBoy ([#238](https://github.com/Tencent/tdesign-mobile-vue/pull/238))
 
-## 🌈 0.9.1 `2022-07-12` 
-### 🚀 Features
-- `Toast`: 新增 overlayProps 属性透传至 overlay 组件，新增 showOverlay 属性控制遮罩层显示 @oceanlvr ([#204](https://github.com/Tencent/tdesign-mobile-vue/pull/204))
-- `Button`: 支持 4 种类型 @anlyyao ([#218](https://github.com/Tencent/tdesign-mobile-vue/pull/218))
-- `Overlay`: 新增组件 @LeeJim ([#223](https://github.com/Tencent/tdesign-mobile-vue/pull/223))
-- `DropdownMenu`: 新增 toggle 方法用于切换菜单 @LeeJim ([#230](https://github.com/Tencent/tdesign-mobile-vue/pull/230))
-- `Tag`: 升级样式以及支持左图标 @anlyyao ([#229](https://github.com/Tencent/tdesign-mobile-vue/pull/229))
-### 🐞 Bug Fixes
-- `Message`: 优化用法，支持 `v-model` @Dengzygx ([#216](https://github.com/Tencent/tdesign-mobile-vue/pull/216))
-- `DateTimePicker`: 修复DateTimePicker组件value为空时无法正常展示的问题 @Dengzygx ([#215](https://github.com/Tencent/tdesign-mobile-vue/pull/215))
-- `Search`: 修复 blur 事件参数返回错误的问题 @Dengzygx ([#217](https://github.com/Tencent/tdesign-mobile-vue/pull/217))
-- `DropdownMenu`: 修复 dropdownmenu-item 的 label 不支持动态更新等问题 @krimeshu ([#220](https://github.com/Tencent/tdesign-mobile-vue/pull/220))
+## 🌈 0.9.1 `2022-07-12` 
+### 🚀 Features
+- `Toast`: 新增 overlayProps 属性透传至 overlay 组件，新增 showOverlay 属性控制遮罩层显示 @oceanlvr ([#204](https://github.com/Tencent/tdesign-mobile-vue/pull/204))
+- `Button`: 支持 4 种类型 @anlyyao ([#218](https://github.com/Tencent/tdesign-mobile-vue/pull/218))
+- `Overlay`: 新增组件 @LeeJim ([#223](https://github.com/Tencent/tdesign-mobile-vue/pull/223))
+- `DropdownMenu`: 新增 toggle 方法用于切换菜单 @LeeJim ([#230](https://github.com/Tencent/tdesign-mobile-vue/pull/230))
+- `Tag`: 升级样式以及支持左图标 @anlyyao ([#229](https://github.com/Tencent/tdesign-mobile-vue/pull/229))
+### 🐞 Bug Fixes
+- `Message`: 优化用法，支持 `v-model` @Dengzygx ([#216](https://github.com/Tencent/tdesign-mobile-vue/pull/216))
+- `DateTimePicker`: 修复DateTimePicker组件value为空时无法正常展示的问题 @Dengzygx ([#215](https://github.com/Tencent/tdesign-mobile-vue/pull/215))
+- `Search`: 修复 blur 事件参数返回错误的问题 @Dengzygx ([#217](https://github.com/Tencent/tdesign-mobile-vue/pull/217))
+- `DropdownMenu`: 修复 dropdownmenu-item 的 label 不支持动态更新等问题 @krimeshu ([#220](https://github.com/Tencent/tdesign-mobile-vue/pull/220))
 - `DropdownMenu`: 修复 radio、checkbox 样式问题 @LeeJim ([#219](https://github.com/Tencent/tdesign-mobile-vue/pull/219))
 
 ## 🌈 0.9.0 `2022-06-30` 
diff --git a/package.json b/package.json
index 23e00d3c..90ae6565 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "tdesign-mobile-vue",
   "purename": "tdesign",
-  "version": "0.10.6",
+  "version": "0.11.0",
   "description": "tdesign-mobile-vue",
   "author": "TDesign Group",
   "homepage": "https://tdesign.tencent.com/mobile-vue/",
diff --git a/src/_common b/src/_common
index 46125ae7..eece3129 160000
--- a/src/_common
+++ b/src/_common
@@ -1,1 +1,1 @@
-Subproject commit 46125ae7b1fe2b18e3bf1cc1d15a04e802a4b06e
+Subproject commit eece31299ae88e2c69d3ab58f4ac1c5c83b71703
diff --git a/src/calendar/calendar.en-US.md b/src/calendar/calendar.en-US.md
new file mode 100644
index 00000000..b9b8e0e7
--- /dev/null
+++ b/src/calendar/calendar.en-US.md
@@ -0,0 +1,25 @@
+:: BASE_DOC ::
+
+## API
+### Calendar Props
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+confirmBtn | String / Object / Slot / Function | '' | Typescript：`string | TNode | ButtonProps | null`，[Button API Documents](./button?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/calendar/type.ts) | N
+firstDayOfWeek | Number | 0 | \- | N
+format | Function | - | Typescript：`(day: TDate) => TDate` `type TDateType = 'selected' | 'disabled' | 'start' | 'centre' | 'end' | ''; ` `interface TDate { date: Date; day: number; type: TDateType; className?: string; prefix?: string; suffix?: string; }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/calendar/type.ts) | N
+maxDate | Number / Date | - | Typescript：`number | Date` | N
+minDate | Number / Date | - | Typescript：`number | Date` | N
+title | String / Slot / Function | - | Typescript：`string | TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+type | String | 'single' | options：single/multiple/range | N
+value | Number / Array / Date | - | Typescript：`number | Date | TCalendarValue[] ` `type TCalendarValue = number | Date`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/calendar/type.ts) | N
+visible | Boolean | false | \- | N
+onConfirm | Function |  | Typescript：`(value: Date) => void`<br/> | N
+onSelect | Function |  | Typescript：`(value: Date) => void`<br/> | N
+
+### Calendar Events
+
+name | params | description
+-- | -- | --
+confirm | `(value: Date)` | \-
+select | `(value: Date)` | \-
diff --git a/src/calendar/calendar.md b/src/calendar/calendar.md
index c26b7d49..2b44bc46 100644
--- a/src/calendar/calendar.md
+++ b/src/calendar/calendar.md
@@ -1,21 +1,25 @@
 :: BASE_DOC ::
 
 ## API
+### Calendar Props
 
-### Calendar Events
-
-名称 | 参数 | 描述
--- | -- | --
-modelValue | Boolean | false | 控制日历显示与隐藏 | Y
-bgColor | String |  #0053DB | 按钮和单元格背景颜色 | N
-confirmBtn | String |  确认 | 确认按钮文案 | N
-head |  Slot / Function |  - | 头部插槽（左上角处，默认不显示任何内容），TS 类型：`string | TNode` [通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
-cell |  Slot / Function |  今天日期 | 自定义单元格 类型：`string | TNode` [通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
-value | String / Date |  new Date | 当前高亮的日期，默认今天 | N
+名称 | 类型 | 默认值 | 说明 | 必传
+-- | -- | -- | -- | --
+confirmBtn | String / Object / Slot / Function | '' | 确认按钮。值为 null 则不显示确认按钮。值类型为字符串，则表示自定义按钮文本，值类型为 Object 则表示透传 Button 组件属性。。TS 类型：`string | TNode | ButtonProps | null`，[Button API Documents](./button?tab=api)。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/calendar/type.ts) | N
+firstDayOfWeek | Number | 0 | 第一天从星期几开始，默认 0 = 周日 | N
+format | Function | - | 用于格式化日期的函数。TS 类型：`(day: TDate) => TDate` `type TDateType = 'selected' | 'disabled' | 'start' | 'centre' | 'end' | ''; ` `interface TDate { date: Date; day: number; type: TDateType; className?: string; prefix?: string; suffix?: string; }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/calendar/type.ts) | N
+maxDate | Number / Date | - | 最大可选的日期，不传则默认半年后。TS 类型：`number | Date` | N
+minDate | Number / Date | - | 最小可选的日期，不传则默认今天。TS 类型：`number | Date` | N
+title | String / Slot / Function | - | 标题，不传默认为“请选择日期”。TS 类型：`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+type | String | 'single' | 日历的选择类型，single = 单选；multiple = 多选; range = 区间选择。可选项：single/multiple/range | N
+value | Number / Array / Date | - | 当前选择的日期，不传则默认今天，当 type = multiple 或 range 时传入数组。TS 类型：`number | Date | TCalendarValue[] ` `type TCalendarValue = number | Date`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/calendar/type.ts) | N
+visible | Boolean | false | 是否显示日历 | N
+onConfirm | Function |  | TS 类型：`(value: Date) => void`<br/>点击确认按钮时触发 | N
+onSelect | Function |  | TS 类型：`(value: Date) => void`<br/>选择日期时触发 | N
 
 ### Calendar Events
 
 名称 | 参数 | 描述
 -- | -- | --
-cell-click | cell: `type CalendarCell = {year: number;month: number;day: number;}`| 点击单元格触发 |
-confirm | dateValue: `type CalendarValue = {year: number;month: number;day: number;week: number;date: string;isToday: boolean;}` | 点击确认按钮触发 |
+confirm | `(value: Date)` | 点击确认按钮时触发
+select | `(value: Date)` | 选择日期时触发
diff --git a/src/calendar/calendar.vue b/src/calendar/calendar.vue
index c8e139c1..d48502cf 100644
--- a/src/calendar/calendar.vue
+++ b/src/calendar/calendar.vue
@@ -1,56 +1,54 @@
 <template>
   <div>
-    <t-popup v-model="flag" placement="bottom">
-      <div class="t-calendar">
-        <div v-if="$slots.head" class="t-calendar__title">
-          <slot name="head"></slot>
+    <t-popup v-model="popup" placement="bottom">
+      <div :class="name">
+        <div :class="`${name}__title`">
+          <slot name="title">{{ title || '请选择日期' }}</slot>
         </div>
-        <div class="t-calendar__text">
-          <div class="t-calendar__text--year" @click="preYear">
-            <chevron-left-icon size="10" />
-            <chevron-left-icon size="10" style="position: relative; right: 6px" />
-          </div>
-          <div class="t-calendar__text--month" @click="preMonth">
-            <chevron-left-icon size="10" />
-          </div>
-          <div class="t-calendar__text--current">{{ time.year }}年{{ time.month + 1 }}月</div>
-          <div class="t-calendar__text--month" @click="nextMonth">
-            <chevron-right-icon size="10" />
-          </div>
-          <div class="t-calendar__text--year" @click="nextYear">
-            <chevron-right-icon size="10" />
-            <chevron-right-icon size="10" style="position: relative; left: -6px" />
-          </div>
+        <CloseIcon :class="`${name}__close-btn`" size="24" @click="popup = false" />
+        <div :class="`${name}__days`">
+          <div v-for="(item, index) in days" :key="index" :class="`${name}__days-item`">{{ item }}</div>
         </div>
-        <div class="t-calendar__days">
-          <div v-for="(item, index) in days" :key="index" class="t-calendar__days--item">{{ item }}</div>
-        </div>
-        <div class="t-calendar__content">
-          <div v-for="i in 6" :key="i" class="t-calendar__content--item">
-            <div v-for="j in 7" :key="j" class="t-calendar__content--item--text">
-              <div
-                v-if="showDays[(i - 1) * 7 + (j - 1)]"
-                :key="j"
-                class="t-calendar__content--item--text--value"
-                :class="{
-                  't-calendar__content--item--text--nocurrent': !isCurrentMonth(showDays[(i - 1) * 7 + (j - 1)]),
-                  't-calendar__content--item--text--today': isToday(showDays[(i - 1) * 7 + (j - 1)]),
-                }"
-                :style="{
-                  background: isToday(showDays[(i - 1) * 7 + (j - 1)]) ? bgColor : '#fff',
-                  color: isToday(showDays[(i - 1) * 7 + (j - 1)]) ? '#fff' : '#303133',
-                }"
-                @click="chooseDay(showDays[(i - 1) * 7 + (j - 1)])"
-              >
-                <slot name="cell" :item="showDays[(i - 1) * 7 + (j - 1)].getDate()">
-                  <div>{{ showDays[(i - 1) * 7 + (j - 1)].getDate() }}</div>
-                </slot>
-              </div>
+        <div :class="`${name}__months`">
+          <template v-for="(item, index) in months" :key="index">
+            <div :class="`${name}__month`">{{ item.year }} 年 {{ item.month + 1 }} 月</div>
+            <div :class="`${name}__dates`">
+              <template v-for="(dateItem, dateIndex) in item.months" :key="dateIndex">
+                <div
+                  :class="{
+                    [`${name}__dates-item`]: true,
+                    [`${name}__dates-item--${dateItem.type}`]: !!dateItem.type,
+                    [`${dateItem.className ?? ''}`]: true,
+                  }"
+                  :style="{
+                    marginLeft: dateIndex === 0 ? `${49 * ((item.weekdayOfFirstDay - firstDayOfWeek + 7) % 7)}px` : 0,
+                  }"
+                  @click="handleSelect(item.year, item.month, dateItem.day, dateItem)"
+                >
+                  <slot name="cell" :item="dateItem">
+                    <div v-if="dateItem.prefix" :class="`${name}__dates-item-prefix`">{{ dateItem.prefix }}</div>
+                    {{ dateItem.day }}
+                    <div
+                      v-if="dateItem.suffix"
+                      :class="{
+                        [`${name}__dates-item-suffix`]: true,
+                        [`${name}__dates-item-suffix--${dateItem.type}`]: !!dateItem.type,
+                      }"
+                    >
+                      {{ dateItem.suffix }}
+                    </div>
+                  </slot>
+                </div>
+              </template>
             </div>
-          </div>
+          </template>
         </div>
-        <div class="t-calendar__btn" @click="confirm">
-          <t-button block :style="{ background: bgColor, color: '#fff' }">{{ confirmBtn }}</t-button>
+        <div class="t-calendar__footer">
+          <slot name="confirmBtn">
+            <template v-if="confirmBtn">
+              <t-button block theme="primary" v-bind="confirmBtn" @click="handleConfirm" />
+            </template>
+          </slot>
         </div>
       </div>
     </t-popup>
@@ -58,183 +56,205 @@
 </template>
 
 <script lang="ts">
-import { computed, toRefs, defineComponent, watch, ref, onMounted } from 'vue';
-import { ChevronRightIcon, ChevronLeftIcon } from 'tdesign-icons-vue-next';
-import props from './props';
+import { defineEmits, defineProps, computed, watch, ref, toRaw } from 'vue';
+import { CloseIcon } from 'tdesign-icons-vue-next';
+import pull from 'lodash/pull';
+
 import config from '../config';
-import type { CalendarCell, CalendarValue } from './type';
+import calendarProps from './props';
+import { TDate, TDateType } from './type';
 
 const { prefix } = config;
 const name = `${prefix}-calendar`;
 
-export default defineComponent({
+export default {
   name,
-  components: {
-    ChevronRightIcon,
-    ChevronLeftIcon,
-  },
-  props,
-  emits: ['cell-click', 'confirm', 'update:modelValue'],
-  setup(props, { emit }) {
-    // 获取时间年月日起
-    const getYearMonthDay = (date: Date) => {
-      const year = date.getFullYear();
-      const month = date.getMonth();
-      const day = date.getDate();
-      return {
-        year,
-        month,
-        day,
-      };
-    };
-    // 显示弹框
-    const flag = ref<boolean>(props.modelValue);
-    // 当前选中日期 默认今天
-    const value = ref<Date>(props.value as Date);
-    // 当前日期
-    const days = ref<string[]>(['日', '一', '二', '三', '四', '五', '六']);
-
-    // 选择的时间
-    const time = ref<CalendarCell>({
-      year: getYearMonthDay(new Date()).year,
-      month: getYearMonthDay(new Date()).month,
-      day: getYearMonthDay(new Date()).day,
-    });
-    const dateValue = ref<CalendarValue>();
-
-    // 获取日期
-    const getDate = (year: number, month: number, day: number) => {
-      return new Date(year, month, day);
-    };
-    // 判断是否是当前月份
-    const isCurrentMonth = (date: Date) => {
-      const { year, month } = getYearMonthDay(date);
-      const { year: y, month: m } = getYearMonthDay(getDate(time.value.year, time.value.month, 1));
-      return year === y && month === m;
-    };
-    // 判断是否是今天
-    const isToday = (date: Date) => {
-      const { year, month, day } = getYearMonthDay(value.value);
-      const { year: y, month: m, day: d } = getYearMonthDay(date);
-      return year === y && month === m && day === d;
-    };
-    // 上一年
-    const preYear = () => {
-      time.value.year--;
-    };
-    // 下一年
-    const nextYear = () => {
-      time.value.year++;
-    };
-    // 上一月
-    const preMonth = () => {
-      const d = getDate(time.value.year, time.value.month, 1);
-      d.setMonth(d.getMonth() - 1);
-      time.value = getYearMonthDay(d);
-    };
-    // 下一月
-    const nextMonth = () => {
-      const d = getDate(time.value.year, time.value.month, 1);
-      d.setMonth(d.getMonth() + 1);
-      time.value = getYearMonthDay(d);
-    };
-    // 选择日期
-    const chooseDay = (item: Date) => {
-      value.value = item;
-      const { year, month, day } = getYearMonthDay(item);
-      emit('cell-click', {
-        year,
-        month: month + 1,
-        day,
-      });
-      const isToday = !!(
-        year === new Date().getFullYear() &&
-        month === new Date().getMonth() &&
-        day === new Date().getDate()
-      );
-      dateValue.value = {
-        year,
-        month: month + 1,
-        day,
-        week: item.getDay(),
-        date: `${year}-${month + 1}-${day}`,
-        isToday,
-      };
-    };
-    // 确认
-    const confirm = () => {
-      flag.value = false;
-      emit('confirm', dateValue.value);
-    };
-    // 格式化时间
-    const formatDate = computed(() => {
-      const { year, month, day } = getYearMonthDay(value.value);
-      return `${year}-${month + 1}-${day}`;
-    });
-    const showDays = computed(() => {
-      const { year, month, day } = getYearMonthDay(getDate(time.value.year, time.value.month, 1));
-      // 获取当月第一天
-      const firstDay = getDate(year, month, 1);
-      // 获取当月第一天是星期几
-      const week = firstDay.getDay();
-      // 开始日期
-      const dis = week * 1000 * 60 * 60 * 24;
-      const startDay = (firstDay as any) - dis;
-      const arr = [];
-      // 循环42天 因为日历是6 * 7的布局
-      for (let i = 0; i < 42; i++) {
-        arr.push(new Date(startDay + i * 1000 * 60 * 60 * 24));
+};
+</script>
+
+<script setup lang="ts">
+const props = defineProps(calendarProps);
+const emit = defineEmits(['select', 'confirm', 'update:modelValue', 'update:value', 'update:visible']);
+
+// 获取时间年月日起
+const getYearMonthDay = (date: Date) => {
+  return {
+    year: date.getFullYear(),
+    month: date.getMonth(),
+    date: date.getDate(),
+  };
+};
+
+const popup = ref<boolean>(props.visible);
+const valueRef = ref(props.value);
+const selectedDate = ref();
+const firstDayOfWeek = computed(() => props.firstDayOfWeek);
+const type = computed(() => props.type);
+const days = computed(() => {
+  const raw = '日一二三四五六';
+  const ans = [];
+  let i = firstDayOfWeek.value % 7;
+
+  while (ans.length < 7) {
+    ans.push(raw[i]);
+    i = (i + 1) % 7;
+  }
+
+  return ans;
+});
+const today = new Date();
+const minDate = props.minDate ? new Date(props.minDate) : today;
+const maxDate = props.maxDate
+  ? new Date(props.maxDate)
+  : new Date(today.getFullYear(), today.getMonth() + 6, today.getDate());
+
+// 获取日期
+const getDate = (year: number, month: number, day: number) => new Date(year, month, day);
+
+// 选择日期
+const handleSelect = (year: number, month: number, date: number, dateItem: TDate) => {
+  if (dateItem.type === 'disabled') return;
+  const selected = new Date(year, month, date);
+
+  if (type.value === 'range' && Array.isArray(selectedDate.value)) {
+    if (selectedDate.value.length === 1) {
+      if (selectedDate.value[0] > selected) {
+        selectedDate.value = [selected];
+      } else {
+        selectedDate.value = [selectedDate.value[0], selected];
       }
-      return arr;
-    });
-    watch(
-      () => props.modelValue,
-      (val) => {
-        flag.value = val;
-      },
-    );
-    watch(
-      () => flag.value,
-      (val) => {
-        emit('update:modelValue', val);
-      },
-    );
-    onMounted(() => {
-      time.value.year = getYearMonthDay(value.value).year;
-      time.value.month = getYearMonthDay(value.value).month;
-      dateValue.value = {
-        year: getYearMonthDay(value.value).year,
-        month: getYearMonthDay(value.value).month + 1,
-        day: value.value.getDate(),
-        week: value.value.getDay(),
-        date: `${getYearMonthDay(value.value).year}-${
-          Number(getYearMonthDay(value.value).month) + 1
-        }-${value.value.getDate()}`,
-        isToday: isToday(value.value),
+    } else {
+      selectedDate.value = [selected];
+    }
+  } else if (props.type === 'multiple') {
+    const newVal = [...selectedDate.value];
+    const index = selectedDate.value.findIndex((item: Date) => isSameDate(item, selected));
+    if (index > -1) {
+      newVal.splice(index, 1);
+    } else {
+      newVal.push(selected);
+    }
+    selectedDate.value = newVal;
+  } else {
+    selectedDate.value = selected;
+  }
+  props.onSelect?.(toRaw(selectedDate.value));
+};
+// 确认
+const handleConfirm = () => {
+  popup.value = false;
+  props.onConfirm?.(toRaw(selectedDate.value));
+};
+const getMonthDates = (date: Date) => {
+  const { year, month } = getYearMonthDay(date);
+  const firstDay = getDate(year, month, 1);
+  const weekdayOfFirstDay = firstDay.getDay();
+  const lastDate = new Date(+getDate(year, month + 1, 1) - 24 * 3600 * 1000).getDate();
+
+  return {
+    year,
+    month,
+    weekdayOfFirstDay,
+    lastDate,
+  };
+};
+type CompareDate = Date | number | { year: number; month: number; date: number };
+const isSameDate = (date1: CompareDate, date2: CompareDate) => {
+  if (date1 instanceof Date) date1 = getYearMonthDay(date1);
+  if (date2 instanceof Date) date2 = getYearMonthDay(date2);
+  const keys = ['year', 'month', 'date'];
+  return keys.every((key) => date1[key] === date2[key]);
+};
+
+const months = computed(() => {
+  const ans = [];
+  let { year: minYear, month: minMonth } = getYearMonthDay(minDate);
+  const { year: maxYear, month: maxMonth } = getYearMonthDay(maxDate);
+  const calcType = (year: number, month: number, date: number): TDateType => {
+    const curDate = new Date(year, month, date, 23, 59, 59);
+
+    if (type.value === 'single') {
+      if (isSameDate({ year, month, date }, selectedDate.value)) return 'selected';
+    }
+    if (type.value === 'multiple') {
+      const hit = selectedDate.value.some((item: Date) => isSameDate({ year, month, date }, item));
+      if (hit) {
+        return 'selected';
+      }
+    }
+    if (type.value === 'range') {
+      if (Array.isArray(selectedDate.value)) {
+        const [startDate, endDate] = selectedDate.value;
+
+        if (startDate && isSameDate({ year, month, date }, startDate)) return 'start';
+        if (endDate && isSameDate({ year, month, date }, endDate)) return 'end';
+        if (startDate && endDate && curDate.getTime() > startDate.getTime() && curDate.getTime() < endDate.getTime())
+          return 'centre';
+      }
+    }
+
+    const minCurDate = new Date(year, month, date, 0, 0, 0);
+    if (curDate.getTime() < minDate.getTime() || minCurDate.getTime() > maxDate.getTime()) {
+      return 'disabled';
+    }
+    return '';
+  };
+
+  while (minYear < maxYear || (minYear === maxYear && minMonth <= maxMonth)) {
+    const target = getMonthDates(getDate(minYear, minMonth, 1));
+    const months: TDate[] = [];
+    for (let i = 1; i <= 31; i++) {
+      if (i > target.lastDate) break;
+      const dateObj: TDate = {
+        date: getDate(minYear, minMonth, i),
+        day: i,
+        type: calcType(minYear, minMonth, i),
       };
+      months.push(props.format ? props.format(dateObj) : dateObj);
+    }
+    ans.push({
+      year: minYear,
+      month: minMonth,
+      months,
+      weekdayOfFirstDay: target.weekdayOfFirstDay,
     });
-    return {
-      name,
-      ...toRefs(props),
-      getYearMonthDay,
-      flag,
-      value,
-      days,
-      time,
-      dateValue,
-      close,
-      getDate,
-      isCurrentMonth,
-      isToday,
-      preYear,
-      nextYear,
-      preMonth,
-      nextMonth,
-      chooseDay,
-      confirm,
-      formatDate,
-      showDays,
-    };
-  },
+    const curDate = getYearMonthDay(getDate(minYear, minMonth + 1, 1));
+    minYear = curDate.year;
+    minMonth = curDate.month;
+  }
+
+  return ans;
+});
+
+const confirmBtn = computed(() => {
+  if (props.confirmBtn === 'string' || props.confirmBtn === '') return { content: props.confirmBtn || '确认' };
+  return props.confirmBtn;
 });
+
+watch(
+  () => props.visible,
+  (val) => {
+    popup.value = val;
+  },
+);
+watch(
+  () => popup.value,
+  (val) => {
+    emit('update:visible', val);
+  },
+);
+
+watch(
+  valueRef,
+  () => {
+    if (Array.isArray(valueRef.value)) {
+      selectedDate.value = valueRef.value.map((item) => new Date(item));
+    } else if (valueRef.value) {
+      selectedDate.value = new Date(valueRef.value);
+    } else {
+      selectedDate.value = props.type === 'multiple' ? [new Date()] : new Date();
+    }
+  },
+  { immediate: true },
+);
 </script>
diff --git a/src/calendar/demos/base.vue b/src/calendar/demos/base.vue
index d5da19b6..76583613 100644
--- a/src/calendar/demos/base.vue
+++ b/src/calendar/demos/base.vue
@@ -1,18 +1,18 @@
 <template>
   <div>
-    <t-calendar v-model="visible" @confirm="handleConfirm" @cell-click="cellClick"></t-calendar>
-    <t-cell title="选择日期" note="请选择日期" arrow @click="visible = true"></t-cell>
+    <t-calendar v-model:visible="visible" @confirm="handleConfirm" @select="handleSelect"></t-calendar>
+    <t-cell title="单个选择日期" arrow @click="visible = true"></t-cell>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { defineComponent, ref } from 'vue';
+import { ref } from 'vue';
 
 const visible = ref(false);
 const handleConfirm = (val: string) => {
   console.log(val);
 };
-const cellClick = (val: any) => {
+const handleSelect = (val: any) => {
   console.log(val);
 };
 </script>
diff --git a/src/calendar/demos/cell.vue b/src/calendar/demos/cell.vue
deleted file mode 100644
index f7c48a94..00000000
--- a/src/calendar/demos/cell.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<template>
-  <div>
-    <t-calendar v-model="visible">
-      <template #cell="{ item }">
-        <div style="text-align: center">
-          {{ item }}
-        </div>
-        <div>描述</div>
-      </template>
-    </t-calendar>
-    <t-cell title="选择日期" note="请选择日期" arrow @click="visible = true"></t-cell>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { defineComponent, ref } from 'vue';
-
-const visible = ref(false);
-</script>
diff --git a/src/calendar/demos/color.vue b/src/calendar/demos/color.vue
deleted file mode 100644
index 142fd677..00000000
--- a/src/calendar/demos/color.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-<template>
-  <div>
-    <t-calendar v-model="visible" bg-color="red"></t-calendar>
-    <t-cell title="选择日期" note="请选择日期" arrow @click="visible = true"></t-cell>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { defineComponent, ref } from 'vue';
-
-const visible = ref(false);
-</script>
diff --git a/src/calendar/demos/custom-button.vue b/src/calendar/demos/custom-button.vue
new file mode 100644
index 00000000..0a38047c
--- /dev/null
+++ b/src/calendar/demos/custom-button.vue
@@ -0,0 +1,15 @@
+<template>
+  <t-calendar v-model:visible="visible" :confirm-btn="confirmBtn" />
+  <t-cell title="自定义按钮" arrow @click="visible = true"></t-cell>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const visible = ref(false);
+const confirmBtn = {
+  content: 'ok',
+  theme: 'danger',
+  shape: 'round',
+};
+</script>
diff --git a/src/calendar/demos/custom-range.vue b/src/calendar/demos/custom-range.vue
new file mode 100644
index 00000000..5a7645af
--- /dev/null
+++ b/src/calendar/demos/custom-range.vue
@@ -0,0 +1,14 @@
+<template>
+  <t-calendar v-model:visible="visible" :value="value" :min-date="minDate" :max-date="maxDate" />
+  <t-cell title="自定义日期区间" arrow @click="visible = true"></t-cell>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const minDate = new Date(2022, 0, 1);
+const maxDate = new Date(2022, 0, 31);
+
+const visible = ref(false);
+const value = new Date(2022, 0, 15);
+</script>
diff --git a/src/calendar/demos/custom-text.vue b/src/calendar/demos/custom-text.vue
new file mode 100644
index 00000000..c302540b
--- /dev/null
+++ b/src/calendar/demos/custom-text.vue
@@ -0,0 +1,47 @@
+<template>
+  <t-calendar v-model:visible="visible" :value="value" :format="format" :min-date="minDate" :max-date="maxDate" />
+  <t-cell title="自定义文案" arrow @click="visible = true"></t-cell>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const minDate = new Date(2022, 1, 1);
+const maxDate = new Date(2022, 2, 15);
+const format = (day) => {
+  const { date } = day;
+  const year = date.getFullYear();
+  const month = date.getMonth() + 1;
+  const curDate = date.getDate();
+
+  day.suffix = '¥60';
+
+  if (year === 2022) {
+    if (month === 2) {
+      const map = {
+        1: '初一',
+        2: '初二',
+        3: '初三',
+        14: '情人节',
+        15: '元宵节',
+      };
+      if (curDate in map) {
+        day.prefix = map[curDate];
+        day.suffix = '¥100';
+        day.className = 'is-holiday';
+      }
+    }
+  }
+
+  return day;
+};
+
+const visible = ref(false);
+const value = new Date(2022, 1, 15);
+</script>
+
+<style lang="less">
+.is-holiday:not(.t-calendar__dates-item--selected) {
+  color: #e34d59;
+}
+</style>
diff --git a/src/calendar/demos/head.vue b/src/calendar/demos/head.vue
deleted file mode 100644
index 0ee0a6aa..00000000
--- a/src/calendar/demos/head.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-<template>
-  <div>
-    <t-calendar v-model="visible">
-      <template #head> 自定义标题 </template>
-    </t-calendar>
-    <t-cell title="选择日期" note="请选择日期" arrow @click="visible = true"></t-cell>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { defineComponent, ref } from 'vue';
-
-const visible = ref(false);
-</script>
diff --git a/src/calendar/demos/mobile.vue b/src/calendar/demos/mobile.vue
index 78fefaea..d714263d 100644
--- a/src/calendar/demos/mobile.vue
+++ b/src/calendar/demos/mobile.vue
@@ -6,16 +6,18 @@
     <tdesign-demo-block title="01 基础" summary="基础用法">
       <div class="Calendar-demo">
         <BaseCalendarDemo />
+        <multipleVue />
+        <rangeDemo />
       </div>
     </tdesign-demo-block>
 
-    <tdesign-demo-block title="02 默认日期" summary="设置默认日期">
-      <div class="Calendar-demo">
-        <ValueCalendarDemo />
-      </div>
+    <tdesign-demo-block title="02 自定义" summary=" ">
+      <customTextDemo />
+      <customButtonVue />
+      <customRangeVue />
     </tdesign-demo-block>
 
-    <tdesign-demo-block title="03 设置标题" summary="自定义标题">
+    <!-- <tdesign-demo-block title="03 设置标题" summary="自定义标题">
       <div class="Calendar-demo">
         <HeadCalendarDemo />
       </div>
@@ -31,13 +33,14 @@
       <div class="Calendar-demo">
         <ColorCalendarDemo />
       </div>
-    </tdesign-demo-block>
+    </tdesign-demo-block> -->
   </div>
 </template>
 <script lang="ts" setup>
 import BaseCalendarDemo from './base.vue';
-import ValueCalendarDemo from './value.vue';
-import HeadCalendarDemo from './head.vue';
-import CellCalendarDemo from './cell.vue';
-import ColorCalendarDemo from './color.vue';
+import multipleVue from './multiple.vue';
+import customTextDemo from './custom-text.vue';
+import rangeDemo from './range.vue';
+import customButtonVue from './custom-button.vue';
+import customRangeVue from './custom-range.vue';
 </script>
diff --git a/src/calendar/demos/multiple.vue b/src/calendar/demos/multiple.vue
new file mode 100644
index 00000000..381c0911
--- /dev/null
+++ b/src/calendar/demos/multiple.vue
@@ -0,0 +1,18 @@
+<template>
+  <div>
+    <t-calendar v-model:visible="visible" type="multiple" @confirm="handleConfirm" @select="handleSelect"></t-calendar>
+    <t-cell title="多个选择日期" arrow @click="visible = true"></t-cell>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const visible = ref(false);
+const handleConfirm = (val: string) => {
+  console.log(val);
+};
+const handleSelect = (val: any) => {
+  console.log(val);
+};
+</script>
diff --git a/src/calendar/demos/range.vue b/src/calendar/demos/range.vue
new file mode 100644
index 00000000..4b6ee0e8
--- /dev/null
+++ b/src/calendar/demos/range.vue
@@ -0,0 +1,18 @@
+<template>
+  <div>
+    <t-calendar v-model:visible="visible" :value="value" type="range" @confirm="handleConfirm"></t-calendar>
+    <t-cell title="区间选择日期" arrow @click="visible = true"></t-cell>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const visible = ref(false);
+const handleConfirm = (val: string) => {
+  console.log(val);
+};
+const today = new Date();
+const tomorrow = new Date(today.getTime() + 5 * 24 * 3600 * 1000);
+const value = ref([today, tomorrow]);
+</script>
diff --git a/src/calendar/demos/value.vue b/src/calendar/demos/value.vue
deleted file mode 100644
index 82db1a79..00000000
--- a/src/calendar/demos/value.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-<template>
-  <div>
-    <t-calendar v-model="visible" :value="value"></t-calendar>
-    <t-cell title="选择日期" note="请选择日期" arrow @click="visible = true"></t-cell>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { defineComponent, ref } from 'vue';
-
-const visible = ref(false);
-const value = new Date('2022-08-08');
-</script>
diff --git a/src/calendar/props.ts b/src/calendar/props.ts
index 760cf3aa..6fe48b28 100644
--- a/src/calendar/props.ts
+++ b/src/calendar/props.ts
@@ -8,41 +8,49 @@ import { TdCalendarProps } from './type';
 import { PropType } from 'vue';
 
 export default {
-  /**
-   * 控制日历显示隐藏
-   */
-  modelValue: {
-    type: Boolean,
-  },
-  // 按钮颜色
-  bgColor: {
-    type: String,
-    default: '#0053DB',
-  },
-  /**
-   * 确认按钮
-   */
+  /** 确认按钮。值为 null 则不显示确认按钮。值类型为字符串，则表示自定义按钮文本，值类型为 Object 则表示透传 Button 组件属性。 */
   confirmBtn: {
-    type: String,
-    default: '确认',
+    type: [String, Object, Function] as PropType<TdCalendarProps['confirmBtn']>,
+    default: '',
   },
-  /**
-   * 第一天从星期几开始，仅在日历展示维度为月份时（mode = month）有效。默认为 1
-   */
+  /** 第一天从星期几开始，默认 0 = 周日 */
   firstDayOfWeek: {
     type: Number,
+    default: 0,
   },
-  /**
-   * 头部插槽（左上角处，默认不显示任何内容）
-   */
-  head: {
-    type: String,
+  /** 用于格式化日期的函数 */
+  format: {
+    type: Function as PropType<TdCalendarProps['format']>,
   },
-  /**
-   * 当前高亮的日期
-   */
+  /** 最大可选的日期，不传则默认半年后 */
+  maxDate: {
+    type: [Number, Date] as PropType<TdCalendarProps['maxDate']>,
+  },
+  /** 最小可选的日期，不传则默认今天 */
+  minDate: {
+    type: [Number, Date] as PropType<TdCalendarProps['minDate']>,
+  },
+  /** 标题，不传默认为“请选择日期” */
+  title: {
+    type: [String, Function] as PropType<TdCalendarProps['title']>,
+  },
+  /** 日历的选择类型，single = 单选；multiple = 多选; range = 区间选择 */
+  type: {
+    type: String as PropType<TdCalendarProps['type']>,
+    default: 'single' as TdCalendarProps['type'],
+    validator(val: TdCalendarProps['type']): boolean {
+      if (!val) return true;
+      return ['single', 'multiple', 'range'].includes(val);
+    },
+  },
+  /** 当前选择的日期，不传则默认今天，当 type = multiple 或 range 时传入数组 */
   value: {
-    type:  Date as unknown as PropType<TdCalendarProps['value']>,
-    default: new Date(),
+    type: [Number, Array, Date] as PropType<TdCalendarProps['value']>,
   },
+  /** 是否显示日历 */
+  visible: Boolean,
+  /** 点击确认按钮时触发 */
+  onConfirm: Function as PropType<TdCalendarProps['onConfirm']>,
+  /** 选择日期时触发 */
+  onSelect: Function as PropType<TdCalendarProps['onSelect']>,
 };
diff --git a/src/calendar/type.ts b/src/calendar/type.ts
index ccc9b0c0..54a9f03d 100644
--- a/src/calendar/type.ts
+++ b/src/calendar/type.ts
@@ -3,50 +3,70 @@
 /**
  * 该文件为脚本自动生成文件，请勿随意修改。如需修改请联系 PMC
  * */
+
+import { ButtonProps } from '../button';
+import { TNode } from '../common';
+
 export interface TdCalendarProps {
   /**
-   * 控制日历显示隐藏
+   * 确认按钮。值为 null 则不显示确认按钮。值类型为字符串，则表示自定义按钮文本，值类型为 Object 则表示透传 Button 组件属性。
+   * @default ''
    */
-  modelValue: boolean;
+  confirmBtn?: string | TNode | ButtonProps | null;
   /**
-   * 背景颜色
+   * 第一天从星期几开始，默认 0 = 周日
+   * @default 0
    */
-  bgColor?: string;
-
+  firstDayOfWeek?: number;
   /**
-   * 确认按钮
+   * 用于格式化日期的函数
    */
-  confirmBtn?: string;
+  format?: (day: TDate) => TDate;
   /**
-   * 第一天从星期几开始，仅在日历展示维度为月份时（mode = month）有效。默认为 1
+   * 最大可选的日期，不传则默认半年后
    */
-  firstDayOfWeek?: number;
+  maxDate?: number | Date;
   /**
-   * 头部插槽（左上角处，默认不显示任何内容）
+   * 最小可选的日期，不传则默认今天
    */
-  head?: string;
+  minDate?: number | Date;
   /**
-   * 当前高亮的日期
+   * 标题，不传默认为“请选择日期”
    */
-  value?: Date;
+  title?: string | TNode;
+  /**
+   * 日历的选择类型，single = 单选；multiple = 多选; range = 区间选择
+   * @default 'single'
+   */
+  type?: 'single' | 'multiple' | 'range';
+  /**
+   * 当前选择的日期，不传则默认今天，当 type = multiple 或 range 时传入数组
+   */
+  value?: number | Date | TCalendarValue[];
+  /**
+   * 是否显示日历
+   * @default false
+   */
+  visible?: boolean;
+  /**
+   * 点击确认按钮时触发
+   */
+  onConfirm?: (value: Date) => void;
+  /**
+   * 选择日期时触发
+   */
+  onSelect?: (value: Date) => void;
 }
 
-export interface DateCellDescription {
-  date: Date;
-  label: string;
-}
+export type TDateType = 'selected' | 'disabled' | 'start' | 'centre' | 'end' | '';
 
-export type CalendarCell = {
-  year: number;
-  month: number;
+export interface TDate {
+  date: Date;
   day: number;
+  type: TDateType;
+  className?: string;
+  prefix?: string;
+  suffix?: string;
 }
 
-export type CalendarValue = {
-  year: number;
-  month: number;
-  day: number;
-  week: number;
-  date: string;
-  isToday: boolean;
-};
\ No newline at end of file
+export type TCalendarValue = number | Date;
diff --git a/src/collapse/demos/action.vue b/src/collapse/demos/action.vue
index ce13799f..3920c839 100644
--- a/src/collapse/demos/action.vue
+++ b/src/collapse/demos/action.vue
@@ -11,14 +11,12 @@
 </template>
 
 <script setup lang="ts">
-import { ref, h } from 'vue';
+import { ref } from 'vue';
 
 const values = ref([0]);
-const handleChange = (val) => {
+const handleChange = (val: number[]) => {
   values.value = val;
 };
-
-// const header = () => h('div', '123');
 </script>
 
 <style scoped lang="less">
diff --git a/src/pull-down-refresh/demos/mobile.vue b/src/pull-down-refresh/demos/mobile.vue
index db849ce2..7eb7d8ed 100644
--- a/src/pull-down-refresh/demos/mobile.vue
+++ b/src/pull-down-refresh/demos/mobile.vue
@@ -4,7 +4,7 @@
     <p class="summary">用于快速刷新页面信息，刷新可以是整页刷新也可以是页面的局部刷新。</p>
     <p class="blank"></p> -->
     <tdesign-demo-block title="" summary="">
-      <t-tabs default-value="first" @on-change="onChange">
+      <t-tabs default-value="first">
         <t-tab-panel value="first" label="基础用法">
           <div class="refresh-content">
             <t-pull-down-refresh
@@ -48,7 +48,7 @@
 </template>
 
 <script lang="ts">
-import { ref, defineComponent, reactive } from 'vue';
+import { ref, defineComponent } from 'vue';
 import { Toast } from '@/components';
 
 export default defineComponent({
diff --git a/src/pull-down-refresh/pull-down-refresh.vue b/src/pull-down-refresh/pull-down-refresh.vue
index 28fe817a..c41a6ee7 100644
--- a/src/pull-down-refresh/pull-down-refresh.vue
+++ b/src/pull-down-refresh/pull-down-refresh.vue
@@ -39,8 +39,8 @@ export default defineComponent({
   name,
   components: { TLoading },
   props: PullDownRefreshProps,
-  emits: ['refresh', 'timeout'],
-  setup(props: any, context: SetupContext) {
+  emits: ['refresh', 'timeout', 'update:value', 'update:modelValue'],
+  setup(props, context: SetupContext) {
     const emitEvent = useEmitEvent(props, context.emit);
 
     // 动态生成style
diff --git a/src/slider/demos/base.vue b/src/slider/demos/base.vue
index e6ac6488..aa14586e 100644
--- a/src/slider/demos/base.vue
+++ b/src/slider/demos/base.vue
@@ -1,9 +1,13 @@
 <template>
-  <t-slider v-model="value" />
+  <t-slider v-model="value" :label="label" :on-change="onChange" />
 </template>
 
 <script lang="ts" setup>
 import { ref } from 'vue';
 
 const value = ref(60);
+const label = ref(false);
+const onChange = ($event: number | number[]) => {
+  console.log(`change to ${$event}`);
+};
 </script>
diff --git a/src/slider/demos/disable.vue b/src/slider/demos/disable.vue
index f1b319fc..29c442ea 100644
--- a/src/slider/demos/disable.vue
+++ b/src/slider/demos/disable.vue
@@ -3,13 +3,13 @@
     <t-slider :default-value="60" disabled />
   </div>
   <div class="section">
-    <t-slider :default-value="30" show-extreme-value :min="30" disabled />
+    <t-slider :default-value="30" :min="30" disabled />
   </div>
   <div class="section">
-    <t-slider :default-value="50" :marks="marks" disabled />
+    <t-slider :default-value="50" :label="label" :marks="marks" disabled />
   </div>
   <div class="section">
-    <t-slider :default-value="[30, 80]" range disabled />
+    <t-slider :default-value="[30, 70]" range disabled />
   </div>
 </template>
 
@@ -17,6 +17,7 @@
 import { ref } from 'vue';
 
 const marks = ref({ 0: '小', 50: '中', 100: '大' });
+const label = ref(false);
 </script>
 
 <style lang="less" scoped>
diff --git a/src/slider/demos/mark.vue b/src/slider/demos/mark.vue
index 2ddefb32..04471ed0 100644
--- a/src/slider/demos/mark.vue
+++ b/src/slider/demos/mark.vue
@@ -1,11 +1,12 @@
 <template>
-  <t-slider v-model="value" :marks="marks" />
+  <t-slider v-model="value" :label="label" :marks="marks" />
 </template>
 
 <script lang="ts" setup>
 import { ref } from 'vue';
 
 const value = ref(50);
+const label = ref(false);
 const marks = ref({ 0: '小', 50: '中', 100: '大' });
 // const marks = ref([0, 50, 100]);
 // const marks = ref({
diff --git a/src/slider/demos/mobile.vue b/src/slider/demos/mobile.vue
index 101da49f..2446a359 100644
--- a/src/slider/demos/mobile.vue
+++ b/src/slider/demos/mobile.vue
@@ -14,7 +14,7 @@
     </tdesign-demo-block>
     <tdesign-demo-block summary="起始非零滑动选择器">
       <div class="slider-demo">
-        <t-slider :default-value="30" :min="30" show-extreme-value> </t-slider>
+        <t-slider :default-value="30" :min="30"> </t-slider>
       </div>
     </tdesign-demo-block>
     <tdesign-demo-block summary="带刻度滑动选择器">
@@ -36,22 +36,11 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
 import BaseDemo from './base.vue';
 import MarkDemo from './mark.vue';
 import ValueDemo from './value.vue';
 import RangeDemo from './range.vue';
 import DisableDemo from './disable.vue';
-
-const value = ref(60);
-const value1 = ref(60);
-const value2 = ref(50);
-const marks = ref({ 0: '小', 50: '中', 100: '大' });
-const rangeValue = ref([30, 70]);
-
-function onChange($event: number | number[]) {
-  console.log(`change to ${$event}`);
-}
 </script>
 
 <style scoped>
diff --git a/src/slider/demos/range.vue b/src/slider/demos/range.vue
index 069bc95f..f42f98cb 100644
--- a/src/slider/demos/range.vue
+++ b/src/slider/demos/range.vue
@@ -1,11 +1,11 @@
 <template>
-  <t-slider v-model="rangeValue" show-value range @change="onChange" />
+  <t-slider v-model="rangeValue" show-extreme-value range @change="onChange" />
 </template>
 
 <script lang="ts" setup>
 import { ref } from 'vue';
 
-const rangeValue = ref([30, 80]);
+const rangeValue = ref([30, 70]);
 
 function onChange($event: number | number[]) {
   console.log(`change to ${$event}`);
diff --git a/src/slider/demos/value.vue b/src/slider/demos/value.vue
index 31ac86a3..e6ac6488 100644
--- a/src/slider/demos/value.vue
+++ b/src/slider/demos/value.vue
@@ -1,5 +1,5 @@
 <template>
-  <t-slider v-model="value" show-value />
+  <t-slider v-model="value" />
 </template>
 
 <script lang="ts" setup>
diff --git a/src/slider/slider.vue b/src/slider/slider.vue
index 47e5e56d..89ba0bb8 100644
--- a/src/slider/slider.vue
+++ b/src/slider/slider.vue
@@ -28,20 +28,23 @@
         </div>
       </div>
     </div>
+    <div v-if="labelContent" :class="`${name}-wrap__value`">
+      <t-node :content="labelContent"></t-node>
+    </div>
     <div v-if="showExtremeValue" :class="`${name}-wrap__value`">{{ max }}</div>
   </div>
 </template>
 
 <script lang="ts">
-import { ref, toRefs, computed, SetupContext, reactive, defineComponent } from 'vue';
+import { ref, toRefs, computed, SetupContext, reactive, defineComponent, getCurrentInstance } from 'vue';
 import config from '../config';
 import props from './props';
 import { useVModel } from '../shared/useVModel';
+import { renderTNode, TNode } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-slider`;
 const { isArray } = Array;
-// label\value\defaultValue
 
 export interface TouchData {
   startValue: number;
@@ -53,6 +56,7 @@ export interface TouchData {
 
 export default defineComponent({
   name,
+  components: { TNode },
   props,
   emits: ['drag-start', 'drag-end', 'update:modelValue', 'change'],
   setup(props, context: SetupContext) {
@@ -61,6 +65,16 @@ export default defineComponent({
     const defaultValue = props.defaultValue || props.min;
     const { value, modelValue, max, min } = toRefs(props);
     const [innerValue, setInnerValue] = useVModel(value, modelValue, defaultValue, props.onChange);
+    const internalInstance = getCurrentInstance();
+    const labelContent = computed(
+      () =>
+        !props.range &&
+        props.label &&
+        (typeof renderTNode(internalInstance, 'label') === 'object'
+          ? renderTNode(internalInstance, 'label')
+          : `${innerValue.value}`),
+    );
+
     const isRange = computed(() => {
       return props.range && isArray(innerValue.value) && innerValue.value.length === 2;
     });
@@ -234,6 +248,7 @@ export default defineComponent({
       barRef,
       dots,
       value: innerValue,
+      labelContent,
       classes,
       handleClass,
       trackStyle,
diff --git a/src/upload/demos/mobile.vue b/src/upload/demos/mobile.vue
index 8cc6cc88..df94ec96 100644
--- a/src/upload/demos/mobile.vue
+++ b/src/upload/demos/mobile.vue
@@ -45,7 +45,7 @@ export default defineComponent({
   components: {},
   setup() {
     const addIcon = () => h(AddIcon);
-    const onFail = ({ e, file }) => {
+    const onFail = ({ file, e }: { file: UploadFile; e: ProgressEvent }) => {
       console.log('---onFail', file, e);
       return null;
     };
@@ -58,7 +58,7 @@ export default defineComponent({
     const onChange = (files: Array<UploadFile>, { e, response, trigger, index, file }: UploadChangeContext) => {
       console.log('====onChange', files, e, response, trigger, index, file);
     };
-    const onPreview = ({ file, e }) => {
+    const onPreview = ({ file, e }: { file: UploadFile; e: MouseEvent }) => {
       console.log('====onPreview', file, e);
     };
     const onSuccess = ({ file, fileList, response, e }: SuccessContext) => {
@@ -71,7 +71,6 @@ export default defineComponent({
       console.log('====onSelectChange', files);
     };
     const action = 'https://service-bv448zsw-1257786608.gz.apigw.tencentcs.com/api/upload-demo';
-    // const action = 'www';
     const max = 10;
     return {
       action,
diff --git a/src/upload/upload.vue b/src/upload/upload.vue
index 1210d666..a7af0687 100644
--- a/src/upload/upload.vue
+++ b/src/upload/upload.vue
@@ -57,9 +57,10 @@ import xhr from '../_common/js/upload/xhr';
 import { useDefault, useEmitEvent, isFunction, isArray, isObject, renderTNode, TNode } from '../shared';
 import { TdUploadProps, UploadFile, RequestMethodResponse, SizeLimitObj } from './type';
 import { SuccessContext, InnerProgressContext } from './interface';
-import props from './props';
+import UploadProps from './props';
 import config from '../config';
 import { isOverSizeLimit } from './util';
+import TImageViewer from '../image-viewer';
 
 const { prefix } = config;
 const name = `${prefix}-upload`;
@@ -70,8 +71,9 @@ export default defineComponent({
     TNode,
     CloseIcon,
     RefreshIcon,
+    TImageViewer,
   },
-  props,
+  props: UploadProps,
   emits: [
     'update:files',
     'update:modelValue',
@@ -83,7 +85,7 @@ export default defineComponent({
     'success',
     'select-change',
   ],
-  setup(props: any, context: SetupContext) {
+  setup(props, context: SetupContext) {
     const emitEvent = useEmitEvent(props, context.emit);
     const [innerFiles, setInnerFiles] = useDefault<TdUploadProps['files'], TdUploadProps>(
       props,
@@ -150,8 +152,8 @@ export default defineComponent({
       });
     };
 
-    const handleReload = (file: any) => {
-      uploadFiles([file]);
+    const handleReload = (file: UploadFile) => {
+      uploadFiles([file.fileRaw]);
     };
 
     const handleChange = () => {
@@ -164,7 +166,8 @@ export default defineComponent({
     };
 
     const formatFileToUploadFile = (files: FileList): File[] => {
-      if (!props.format || !isFunction(props.format)) {
+      const { format } = props;
+      if (!format || !isFunction(format)) {
         const res = [];
         for (let i = 0; i < files.length; i++) {
           res.push(files[i]);
@@ -174,7 +177,7 @@ export default defineComponent({
 
       const NewFiles = [...files];
       NewFiles.forEach((item) => {
-        item = props.format?.(item);
+        item = format(item) as any;
       });
       return NewFiles;
     };
@@ -232,7 +235,7 @@ export default defineComponent({
           status: 'waiting',
         };
         const reader = new FileReader();
-        reader.readAsDataURL(fileRaw as any);
+        reader.readAsDataURL(fileRaw);
         reader.onload = (event: ProgressEvent<FileReader>) => {
           uploadFile.url = event.target?.result as string;
         };
@@ -311,6 +314,7 @@ export default defineComponent({
             file: file.fileRaw,
             ...props.data,
           },
+          file,
           method: props.method,
           headers: props.headers || {},
           withCredentials: props.withCredentials,
@@ -379,7 +383,7 @@ export default defineComponent({
       }
       file.url = res?.url || file.url;
       // 从待上传文件队列中移除上传成功的文件
-      const index = findIndex(toUploadFiles.value, (o: any) => o.name === file.name);
+      const index = findIndex(toUploadFiles.value, (o: UploadFile) => o.name === file.name);
       toUploadFiles.value.splice(index, 1);
       // 上传成功的文件发送到 files
       const newFile = { ...file, response: res };
@@ -399,7 +403,7 @@ export default defineComponent({
       file: UploadFile;
       response?: any;
       resFormatted?: boolean;
-    }): any => {
+    }) => {
       const { event, file, response, resFormatted } = options;
       file.status = 'fail';
       let res = response;


Test patch:
diff --git a/src/pull-down-refresh/__test__/__snapshots__/index.test.jsx.snap b/src/pull-down-refresh/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..dba2a2c9
--- /dev/null
+++ b/src/pull-down-refresh/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,26 @@
+// Vitest Snapshot v1
+
+exports[`PullDownRefresh > : props > : others 1`] = `
+<div
+  class="t-pull-down-refresh"
+>
+  <div
+    class="t-pull-down-refresh__track"
+    style="transition-duration: 300ms; transform: translate3d(0, 0px, 0);"
+  >
+    <div
+      class="t-pull-down-refresh__max"
+      style="height: 80px;"
+    >
+      <div
+        class="t-pull-down-refresh__loading"
+        style="height: 50px;"
+      >
+        <div />
+      </div>
+    </div>
+    
+    
+  </div>
+</div>
+`;
diff --git a/src/pull-down-refresh/__test__/index.test.jsx b/src/pull-down-refresh/__test__/index.test.jsx
new file mode 100644
index 00000000..5fa3eed9
--- /dev/null
+++ b/src/pull-down-refresh/__test__/index.test.jsx
@@ -0,0 +1,129 @@
+import { nextTick, ref } from 'vue';
+import { mount } from '@vue/test-utils';
+import { beforeAll, vi } from 'vitest';
+import PullDownRefresh from '../index';
+import { MockResizeObserver, trigger } from './utils';
+
+describe('PullDownRefresh', () => {
+  beforeAll(() => {
+    global.ResizeObserver = MockResizeObserver;
+  });
+
+  describe(': props', () => {
+    it(': loadingBarHeight', async () => {
+      const wrapper = mount(<PullDownRefresh value={true} loadingBarHeight={30} />);
+      const loadingBar = wrapper.find('.t-pull-down-refresh__loading');
+      expect(window.getComputedStyle(loadingBar.element).height).toBe(`${30}px`);
+
+      await wrapper.setProps({ loadingBarHeight: '90px' });
+      expect(window.getComputedStyle(loadingBar.element).height).toBe(`${90}px`);
+    });
+
+    it(': others', async () => {
+      const status = ref(false);
+      const loadingProps = {
+        theme: 'circular',
+      };
+      const loadingTexts = ['a', 'b', 'c', 'd'];
+      const maxBarHeight = ref(80);
+      const onChange = vi.fn((value) => {
+        status.value = value;
+      });
+      const onRefresh = vi.fn(() => {
+        status.value = true;
+        setTimeout(() => {
+          status.value = false;
+        }, 2000);
+      });
+
+      const wrapper = mount(
+        <PullDownRefresh
+          value={status.value}
+          loadingProps={loadingProps}
+          loadingTexts={loadingTexts}
+          maxBarHeight={maxBarHeight.value}
+          onChange={onChange}
+          onRefresh={onRefresh}
+        />,
+      );
+      const track = wrapper.find('.t-pull-down-refresh__track');
+      const maxBar = wrapper.find('.t-pull-down-refresh__max');
+
+      await nextTick();
+      maxBar.trigger('resize', {
+        height: 80,
+      });
+
+      trigger(track, 'touchstart', 50, 0);
+      await wrapper.setProps({ value: true, maxBarHeight: '80px' });
+      trigger(track, 'touchmove', 50, 20);
+      trigger(track, 'touchmove', 50, 70);
+      trigger(track, 'touchmove', 50, 120);
+      trigger(track, 'touchmove', 50, 170);
+      expect(wrapper.vm.loadingText).toBe('b'); // loosing
+      expect(window.getComputedStyle(maxBar.element).height).toBe(`${80}px`);
+      trigger(track, 'touchend', 50, 170);
+      expect(wrapper.element).toMatchSnapshot();
+
+      await new Promise((resolve, reject) => {
+        setTimeout(() => resolve(), 350); // 大于 300ms
+      });
+    });
+  });
+
+  describe(': events', () => {
+    it('functions and refreshTimeout', async () => {
+      const status = ref(false);
+      const refreshTimeout = 500;
+
+      const onChange = vi.fn((value) => {
+        status.value = value;
+      });
+      const onRefresh = vi.fn(() => {
+        status.value = true;
+        setTimeout(() => {
+          status.value = false;
+        }, 1000); // 大于设定值
+      });
+      const onTimeout = vi.fn();
+
+      const wrapper = mount(
+        <PullDownRefresh
+          value={status.value}
+          refreshTimeout={refreshTimeout}
+          onChange={onChange}
+          onRefresh={onRefresh}
+          onTimeout={onTimeout}
+        />,
+      );
+
+      const target = wrapper.find('.t-pull-down-refresh__track');
+      const maxBar = wrapper.find('.t-pull-down-refresh__max');
+
+      await nextTick();
+      maxBar.trigger('resize', {
+        height: 80,
+      });
+
+      trigger(target, 'touchstart', 50, 0);
+      await wrapper.setProps({ value: true });
+      trigger(target, 'touchmove', 50, 20);
+      trigger(target, 'touchmove', 50, 70);
+      trigger(target, 'touchmove', 50, 120);
+      trigger(target, 'touchmove', 50, 170);
+      trigger(target, 'touchend', 50, 180);
+
+      await new Promise((resolve, reject) => {
+        setTimeout(() => resolve(), 1000); // 超时 1000ms
+      });
+
+      expect(wrapper.emitted()).toHaveProperty('update:value'); // useModel 受控模式
+
+      expect(onChange).toHaveBeenCalled();
+      expect(onRefresh).toHaveBeenCalled();
+      expect(onTimeout).toHaveBeenCalled();
+
+      wrapper.unmount();
+    });
+  });
+});
diff --git a/src/pull-down-refresh/__test__/utils.ts b/src/pull-down-refresh/__test__/utils.ts
new file mode 100644
index 00000000..3f73b99c
--- /dev/null
+++ b/src/pull-down-refresh/__test__/utils.ts
@@ -0,0 +1,97 @@
+import { ComponentPublicInstance, nextTick } from 'vue';
+import { VueWrapper, DOMWrapper } from '@vue/test-utils';
+
+function getTouch(el: Element | Window, x: number, y: number) {
+  return {
+    identifier: Date.now(),
+    target: el,
+    pageX: x,
+    pageY: y,
+    clientX: x,
+    clientY: y,
+    radiusX: 2.5,
+    radiusY: 2.5,
+    rotationAngle: 10,
+    force: 0.5,
+  };
+}
+
+// 模拟 touch 事件
+export function trigger(
+  wrapper: VueWrapper<ComponentPublicInstance<any, any, any>> | DOMWrapper<Element> | Element | Window,
+  eventName: string,
+  x = 0,
+  y = 0,
+  options: any = {},
+) {
+  const el = 'element' in wrapper ? wrapper.element : wrapper;
+  const touchList = options.touchList || [getTouch(el, x, y)];
+
+  if (options.x || options.y) {
+    touchList.push(getTouch(el, options.x, options.y));
+  }
+
+  const event = document.createEvent('CustomEvent');
+  event.initCustomEvent(eventName, true, true, {});
+
+  Object.assign(event, {
+    clientX: x,
+    clientY: y,
+    touches: touchList,
+    targetTouches: touchList,
+    changedTouches: touchList,
+  });
+
+  el.dispatchEvent(event);
+
+  return nextTick();
+}
+
+/**
+ * PullDownRefresh 使用了 [`useElementSize()`](https://github.com/vueuse/vueuse/blob/658374fd12/packages/core/useElementSize/index.ts)，
+ * 该函数调用了 [`useResizeObserver()`](https://github.com/vueuse/vueuse/blob/658374fd12fbce2ac6127a9fb9bca52fb2137505/packages/core/useResizeObserver/index.ts)。
+ *
+ * 而 jsdom 中 ResizeObserver 无效，见： https://github.com/jsdom/jsdom/issues/3368
+ *
+ * @description 模拟实现 ResizeObserver
+ * @params callback `new ResizeObserver(callback)` 中的回调，签名如下：
+ *
+ * ```ts
+ * ResizeObserverCallback = (entries: ReadonlyArray<ResizeObserverEntry>, observer: ResizeObserver) => void
+ * ```
+ */
+export class MockResizeObserver {
+  _callback: Function;
+
+  _element!: HTMLElement;
+
+  constructor(callback: Function) {
+    this._callback = callback;
+  }
+
+  observe(element: HTMLElement) {
+    this._element = element;
+    this._element.addEventListener('resize', this.trigger);
+  }
+
+  unobserve() {
+    this._element.removeEventListener('resize', this.trigger);
+  }
+
+  disconnect() {
+    this._element.removeEventListener('resize', this.trigger);
+  }
+
+  /**
+   * @description 手动触发 resize，提供参数。本例 `maxBarHeight` 始终为 `useElementSize()` 的默认初始值 `initialSize`（`{ width: 0, height: 0 }`），模拟返回有效值即可
+   * @param event 手动触发
+   */
+  trigger = (event: UIEvent) => {
+    const { width = 0, height = 0 } = event as { width?: number; height?: number };
+    this._callback([
+      {
+        contentRect: { width, height },
+      },
+    ]);
+  };
+}
diff --git a/src/slider/__test__/__snapshots__/demo.test.jsx.snap b/src/slider/__test__/__snapshots__/demo.test.jsx.snap
index 730cd9c6..8af47150 100644
--- a/src/slider/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/slider/__test__/__snapshots__/demo.test.jsx.snap
@@ -33,6 +33,7 @@ exports[`Slider > Slider baseVue demo works fine 1`] = `
     </div>
   </div>
   <!--v-if-->
+  <!--v-if-->
 </div>
 `;
 
@@ -77,6 +78,11 @@ exports[`Slider > Slider disableVue demo works fine 1`] = `
           
         </div>
       </div>
+      <div
+        class="t-slider-wrap__value"
+      >
+        60
+      </div>
       <!--v-if-->
     </div>
   </div>
@@ -85,14 +91,10 @@ exports[`Slider > Slider disableVue demo works fine 1`] = `
     data-v-65b1e7f4=""
   >
     <div
-      class="t-slider-wrap t-is-disabled t-is-value"
+      class="t-slider-wrap t-is-disabled"
       data-v-65b1e7f4=""
     >
-      <div
-        class="t-slider-wrap__value--left"
-      >
-        30
-      </div>
+      <!--v-if-->
       <div
         class="t-slider"
       >
@@ -123,8 +125,9 @@ exports[`Slider > Slider disableVue demo works fine 1`] = `
       <div
         class="t-slider-wrap__value"
       >
-        100
+        30
       </div>
+      <!--v-if-->
     </div>
   </div>
   <div
@@ -182,6 +185,7 @@ exports[`Slider > Slider disableVue demo works fine 1`] = `
         </div>
       </div>
       <!--v-if-->
+      <!--v-if-->
     </div>
   </div>
   <div
@@ -203,7 +207,7 @@ exports[`Slider > Slider disableVue demo works fine 1`] = `
         <!-- 滑块长度 -->
         <div
           class="t-slider__track"
-          style="left: 30%; width: 50%;"
+          style="left: 30%; width: 40%;"
         />
         <!-- 滑块操作 -->
         
@@ -213,7 +217,7 @@ exports[`Slider > Slider disableVue demo works fine 1`] = `
         />
         <div
           class="t-slider__handle"
-          style="left: 80%;"
+          style="left: 70%;"
         />
         
         <!-- 刻度内容 -->
@@ -225,6 +229,7 @@ exports[`Slider > Slider disableVue demo works fine 1`] = `
         </div>
       </div>
       <!--v-if-->
+      <!--v-if-->
     </div>
   </div>
   
@@ -282,6 +287,7 @@ exports[`Slider > Slider markVue demo works fine 1`] = `
     </div>
   </div>
   <!--v-if-->
+  <!--v-if-->
 </div>
 `;
 
@@ -354,6 +360,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
           </div>
         </div>
         <!--v-if-->
+        <!--v-if-->
       </div>
     </div>
     
@@ -376,7 +383,6 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
       <div
         class="t-slider-wrap"
         data-v-98828574=""
-        show-value=""
       >
         <!--v-if-->
         <div
@@ -406,6 +412,11 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
             
           </div>
         </div>
+        <div
+          class="t-slider-wrap__value"
+        >
+          60
+        </div>
         <!--v-if-->
       </div>
     </div>
@@ -427,14 +438,10 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
       data-v-98828574=""
     >
       <div
-        class="t-slider-wrap t-is-value"
+        class="t-slider-wrap"
         data-v-98828574=""
       >
-        <div
-          class="t-slider-wrap__value--left"
-        >
-          30
-        </div>
+        <!--v-if-->
         <div
           class="t-slider"
         >
@@ -465,8 +472,9 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
         <div
           class="t-slider-wrap__value"
         >
-          100
+          30
         </div>
+        <!--v-if-->
       </div>
     </div>
     
@@ -537,6 +545,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
           </div>
         </div>
         <!--v-if-->
+        <!--v-if-->
       </div>
     </div>
     
@@ -557,11 +566,14 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
       data-v-98828574=""
     >
       <div
-        class="t-slider-wrap"
+        class="t-slider-wrap t-is-value"
         data-v-98828574=""
-        show-value=""
       >
-        <!--v-if-->
+        <div
+          class="t-slider-wrap__value--left"
+        >
+          0
+        </div>
         <div
           class="t-slider"
         >
@@ -572,7 +584,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
           <!-- 滑块长度 -->
           <div
             class="t-slider__track"
-            style="left: 30%; width: 50%;"
+            style="left: 30%; width: 40%;"
           />
           <!-- 滑块操作 -->
           
@@ -582,7 +594,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
           />
           <div
             class="t-slider__handle"
-            style="left: 80%;"
+            style="left: 70%;"
           />
           
           <!-- 刻度内容 -->
@@ -594,6 +606,11 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
           </div>
         </div>
         <!--v-if-->
+        <div
+          class="t-slider-wrap__value"
+        >
+          100
+        </div>
       </div>
     </div>
     
@@ -654,6 +671,11 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
               
             </div>
           </div>
+          <div
+            class="t-slider-wrap__value"
+          >
+            60
+          </div>
           <!--v-if-->
         </div>
       </div>
@@ -662,14 +684,10 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
         data-v-65b1e7f4=""
       >
         <div
-          class="t-slider-wrap t-is-disabled t-is-value"
+          class="t-slider-wrap t-is-disabled"
           data-v-65b1e7f4=""
         >
-          <div
-            class="t-slider-wrap__value--left"
-          >
-            30
-          </div>
+          <!--v-if-->
           <div
             class="t-slider"
           >
@@ -700,8 +718,9 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
           <div
             class="t-slider-wrap__value"
           >
-            100
+            30
           </div>
+          <!--v-if-->
         </div>
       </div>
       <div
@@ -759,6 +778,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
             </div>
           </div>
           <!--v-if-->
+          <!--v-if-->
         </div>
       </div>
       <div
@@ -780,7 +800,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
             <!-- 滑块长度 -->
             <div
               class="t-slider__track"
-              style="left: 30%; width: 50%;"
+              style="left: 30%; width: 40%;"
             />
             <!-- 滑块操作 -->
             
@@ -790,7 +810,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
             />
             <div
               class="t-slider__handle"
-              style="left: 80%;"
+              style="left: 70%;"
             />
             
             <!-- 刻度内容 -->
@@ -802,6 +822,7 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
             </div>
           </div>
           <!--v-if-->
+          <!--v-if-->
         </div>
       </div>
       
@@ -813,10 +834,13 @@ exports[`Slider > Slider mobileVue demo works fine 1`] = `
 
 exports[`Slider > Slider rangeVue demo works fine 1`] = `
 <div
-  class="t-slider-wrap"
-  show-value=""
+  class="t-slider-wrap t-is-value"
 >
-  <!--v-if-->
+  <div
+    class="t-slider-wrap__value--left"
+  >
+    0
+  </div>
   <div
     class="t-slider"
   >
@@ -827,7 +851,7 @@ exports[`Slider > Slider rangeVue demo works fine 1`] = `
     <!-- 滑块长度 -->
     <div
       class="t-slider__track"
-      style="left: 30%; width: 50%;"
+      style="left: 30%; width: 40%;"
     />
     <!-- 滑块操作 -->
     
@@ -837,7 +861,7 @@ exports[`Slider > Slider rangeVue demo works fine 1`] = `
     />
     <div
       class="t-slider__handle"
-      style="left: 80%;"
+      style="left: 70%;"
     />
     
     <!-- 刻度内容 -->
@@ -849,13 +873,17 @@ exports[`Slider > Slider rangeVue demo works fine 1`] = `
     </div>
   </div>
   <!--v-if-->
+  <div
+    class="t-slider-wrap__value"
+  >
+    100
+  </div>
 </div>
 `;
 
 exports[`Slider > Slider valueVue demo works fine 1`] = `
 <div
   class="t-slider-wrap"
-  show-value=""
 >
   <!--v-if-->
   <div
@@ -885,6 +913,11 @@ exports[`Slider > Slider valueVue demo works fine 1`] = `
       
     </div>
   </div>
+  <div
+    class="t-slider-wrap__value"
+  >
+    60
+  </div>
   <!--v-if-->
 </div>
 `;
diff --git a/src/slider/__test__/index.test.jsx b/src/slider/__test__/index.test.jsx
new file mode 100644
index 00000000..fe0829ea
--- /dev/null
+++ b/src/slider/__test__/index.test.jsx
@@ -0,0 +1,182 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import Slider from '../slider.vue';
+import { trigger } from '../../image-viewer/__test__/touch';
+
+const classPrefix = 't-slider';
+const move = async (target, distance) => {
+  await trigger(target, 'touchstart', 0, 0);
+  await trigger(target, 'touchmove', distance, 0);
+  await trigger(target, 'touchend', distance, 0);
+};
+
+describe('slider', () => {
+  describe('props', () => {
+    it(': range', async () => {
+      const value = [30, 70];
+      const range = true;
+      const disabled = true;
+      const onChange = vi.fn();
+      const wrapper = mount(Slider, {
+        props: {
+          value,
+          range,
+          disabled,
+          onChange
+        },
+      });
+
+      const $slider = wrapper.find(`.${classPrefix}`);
+      const $bar = wrapper.findAll(`.${classPrefix}__handle`);
+      expect($bar.length).toStrictEqual(value.length);
+
+      const event = {
+        clientX: 82,
+        stopPropagation: vi.fn()
+      }
+
+      // disabled = true, 禁止点击
+      await $slider.trigger('click', {...event})
+      expect(onChange).toHaveBeenCalledTimes(0)
+
+      // disabled = false
+      await wrapper.setProps({
+        disabled: false
+      });
+      await $slider.trigger('click', {...event})
+      expect(onChange).toHaveBeenCalledTimes(1)
+    });
+
+    it(': showExtremeValue', async () => {
+      const value = [30, 70];
+      const showExtremeValue = true;
+      const wrapper = mount(Slider, {
+        props: {
+          label: false,
+          value,
+          showExtremeValue
+        },
+
+      });
+      const $left = wrapper.find(`.${classPrefix}-wrap__value--left`);
+      const $right = wrapper.find(`.${classPrefix}-wrap__value`);
+
+      // min = 0, max = 100 (默认)
+      expect(Number($left.text())).toStrictEqual(0);
+      expect(Number($right.text())).toStrictEqual(100);
+
+      const newMin = 30;
+      const newMax = 110;
+      await wrapper.setProps({
+        min: newMin,
+        max: newMax,
+      });
+      expect(Number($left.text())).toStrictEqual(newMin);
+      expect(Number($right.text())).toStrictEqual(newMax);
+    });
+
+    it(': marks', async () => {
+      const value = [30, 70];
+      const marks = [0, 25, 50, 75, 100]; // Array
+      const label = false;
+      const wrapper = mount(Slider, {
+        props: {
+          value,
+          marks,
+          label,
+        },
+      });
+      const $mark = wrapper.findAll(`.${classPrefix}__mark-text`);
+      expect($mark.length).toStrictEqual(marks.length);
+      $mark.forEach((item, index) => {
+        expect(Number(item.text())).toStrictEqual(marks[index]);
+        expect(item.attributes('style').includes(`left: ${marks[index]}%;`)).toBeTruthy();
+      })
+
+      const newMarks = { 0: '小', 50: '中', 100: '大' }; // Object
+      await wrapper.setProps({
+        marks: newMarks
+      });
+
+      const $newMarks = wrapper.findAll(`.${classPrefix}__mark-text`);
+      const marks2 = Object.entries(newMarks);
+      expect($newMarks.length).toEqual(marks2.length);
+      $newMarks.forEach((item, index) => {
+        expect(item.text()).toStrictEqual(marks2[index][1]);
+        expect(item.attributes('style').includes(`left: ${marks2[index][0]}%;`)).toBeTruthy();
+      })
+    });
+
+    it(': label', async () => {
+      const value = 0;
+      const wrapper = mount(Slider, {
+        props: {
+          value,
+        },
+      });
+
+      // min = 0, max = 100, label = true (默认)
+      // label && 滑块条
+      const $label = wrapper.find(`.${classPrefix}-wrap__value`);
+      const $bar = wrapper.findAll(`.${classPrefix}__handle`);
+      expect($bar.length).toStrictEqual(1);
+      expect(Number($label.text())).toEqual(value);
+      expect($bar[0].attributes('style').includes(`left: ${value}%;`)).toBeTruthy();
+
+      const newValue = 30;
+      await wrapper.setProps({
+        value: newValue
+      });
+      expect(Number($label.text())).toEqual(newValue);
+      expect($bar[0].attributes('style').includes(`left: ${newValue}%;`)).toBeTruthy();
+    });
+  });
+
+  describe('slots', () => {
+    it(': label', () => {
+      const label = `<div class="t-slider__label">label</div>`
+      const wrapper = mount(Slider, {
+        slots: {
+          label,
+        },
+      });
+      const $label = wrapper.find(`.${classPrefix}-wrap__value`);
+      expect($label.html().includes(label)).toBeTruthy();
+    });
+  });
+
+  describe('event', () => {
+    it(': change && drag-start && drag-end', async () => {
+      const onDragStart = vi.fn();
+      const onDragEnd = vi.fn();
+      const onChange = vi.fn();
+      const value = 0;
+      const disabled = true;
+      const wrapper = mount(Slider, {
+        props: {
+          value,
+          disabled,
+          onDragStart,
+          onDragEnd,
+          onChange,
+        },
+      });
+      const $bar = wrapper.find(`.${classPrefix}__handle`);
+
+      // disabled = true, 禁止滑动
+      await move($bar, 120);
+      expect(onDragStart).toHaveBeenCalledTimes(0);
+      expect(onDragEnd).toHaveBeenCalledTimes(0);
+      expect(onChange).toHaveBeenCalledTimes(0);
+
+      // disabled = false
+      await wrapper.setProps({
+        disabled: false
+      });
+      await move($bar, 120);
+      expect(onDragStart).toHaveBeenCalledTimes(1)
+      expect(onDragEnd).toHaveBeenCalledTimes(1)
+      expect(onChange).toHaveBeenCalledTimes(2)
+    });
+  });
+})
diff --git a/src/upload/__test__/index.test.jsx b/src/upload/__test__/index.test.jsx
new file mode 100644
index 00000000..7a8a1ce1
--- /dev/null
+++ b/src/upload/__test__/index.test.jsx
@@ -0,0 +1,451 @@
+import { ref, nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import { AddIcon, CloseIcon, RefreshIcon, UploadIcon } from 'tdesign-icons-vue-next';
+import Upload from '../upload.vue';
+
+const mockFileFoo = new File([new ArrayBuffer(3)], 'foo.png', {
+  type: 'image/png',
+});
+
+const mockFileBar = new File([new ArrayBuffer(3)], 'bar.png', {
+  type: 'image/png',
+});
+
+const action = 'http://service-bv448zsw-1257786608.gz.apigw.tencentcs.com/api/upload-demo';
+
+const requestMethod = (file) =>
+  new Promise((resolve, reject) => {
+    resolve({
+      status: 'success',
+      response: { url: 'https://tdesign.gtimg.com/site/source/figma-pc.png' },
+    });
+  });
+
+const sleep = (timeout) => {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      resolve();
+    }, timeout);
+  });
+};
+
+const triggerUploadFile = (node, fileList) => {
+  const target = node.find('input');
+  const { element } = target;
+
+  // 判断 HTMLInputElement 对象是否被处理过
+  if (target.element._hijack) {
+    element.files = [...element.files, ...fileList];
+  } else {
+    target.element._hijack = true;
+    Object.defineProperty(target.element, 'files', {
+      value: fileList,
+      writable: true,
+    });
+    // handleChange() 之后会执行 input.value = ''，因此需要清空 files，否则会持续积攒
+    Object.defineProperty(target.element, 'value', {
+      set(value) {
+        if (value === '') {
+          target.element.files = [];
+        }
+      },
+    });
+  }
+
+  target.trigger('change');
+};
+
+describe('Upload', () => {
+  describe('props', () => {
+    it(': action', async () => {
+      const onProgress = vi.fn();
+
+      const wrapper = mount(Upload, {
+        props: {
+          action,
+          onProgress,
+        },
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+
+      await sleep(1000);
+      expect(onProgress).toHaveBeenCalled();
+    });
+
+    it(': allowUploadDuplicateFile', async () => {
+      const props = {
+        autoUpload: false,
+        allowUploadDuplicateFile: true,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+      triggerUploadFile(wrapper, [mockFileFoo]);
+
+      await nextTick();
+      expect(wrapper.vm.toUploadFiles.length).toBe(2);
+
+      wrapper.setProps({
+        allowUploadDuplicateFile: false,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+
+      await nextTick();
+      expect(wrapper.vm.toUploadFiles.length).toBe(2);
+    });
+
+    it(': disabled', async () => {
+      const onSelectChange = vi.fn();
+
+      const props = {
+        autoUpload: false,
+        disabled: true,
+        onSelectChange,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      await wrapper.findComponent(Upload).trigger('click');
+      expect(onSelectChange).not.toHaveBeenCalled();
+    });
+
+    it(': files', async () => {
+      const files = ref([]);
+
+      const wrapper = mount(<Upload requestMethod={requestMethod} v-model={files.value} />);
+
+      await wrapper.findComponent(Upload).setValue([mockFileFoo]);
+      expect(files.value.length).toBe(1);
+    });
+
+    it(': defaultFiles', async () => {
+      const defaultFiles = ref([]);
+
+      const wrapper = mount({
+        setup() {
+          return () => <Upload requestMethod={requestMethod} defaultFiles={defaultFiles.value} />;
+        },
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+      expect(defaultFiles.value.length).toBe(0);
+    });
+
+    it(': gridConfig', async () => {
+      const gridConfig = ref({
+        column: 2,
+      });
+
+      const props = {
+        requestMethod,
+        gridConfig: gridConfig.value,
+        multiple: true,
+        allowUploadDuplicateFile: true,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      await wrapper.findComponent(Upload).setValue([mockFileFoo, mockFileBar, mockFileFoo, mockFileBar]);
+
+      expect(wrapper.vm.itemStyle).toStrictEqual({ flexBasis: `${100 / 2}%` });
+
+      gridConfig.value.column = 4;
+      expect(wrapper.vm.itemStyle).toStrictEqual({ flexBasis: `${100 / 4}%` });
+    });
+
+    it(': max', async () => {
+      const props = {
+        requestMethod,
+        allowUploadDuplicateFile: true,
+        max: 3,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo, mockFileBar, mockFileFoo, mockFileBar]);
+      await nextTick();
+      expect(wrapper.vm.toUploadFiles.length).toBe(3);
+    });
+
+    it(': multiple', async () => {
+      const props = {
+        autoUpload: false,
+        multiple: true,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo, mockFileBar]);
+      await nextTick();
+      expect(wrapper.vm.toUploadFiles.length).toBe(2);
+    });
+
+    it(': requestMethod', async () => {
+      const onSuccess = vi.fn();
+      const onFail = vi.fn();
+      const response = ref({
+        status: 'success',
+        response: { url: 'https://tdesign.gtimg.com/site/source/figma-pc.png' },
+      });
+
+      const props = {
+        allowUploadDuplicateFile: true,
+        onSuccess,
+        onFail,
+        requestMethod: (file) => new Promise((resolve, reject) => resolve(response.value)),
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+      await sleep(0);
+      expect(onSuccess).toHaveBeenCalledTimes(1);
+
+      response.value = {
+        status: 'success',
+        error: 'bar',
+        response: {
+          error: 'foo',
+        },
+      };
+      triggerUploadFile(wrapper, [mockFileFoo]);
+      await sleep(0);
+      expect(onSuccess).toHaveBeenCalledTimes(1);
+
+      response.value = {
+        status: 'fail',
+        error: 'bar',
+      };
+      triggerUploadFile(wrapper, [mockFileBar]);
+      await sleep(0);
+      expect(onFail).toHaveBeenCalledTimes(2);
+
+      response.value = undefined;
+      triggerUploadFile(wrapper, [mockFileBar]);
+      await sleep(0);
+      expect(onFail).toHaveBeenCalledTimes(2);
+
+      response.value = {
+        error: 'bar',
+      };
+      triggerUploadFile(wrapper, [mockFileBar]);
+      await sleep(0);
+      expect(onFail).toHaveBeenCalledTimes(2);
+
+      response.value = {
+        status: 'yes',
+      };
+      triggerUploadFile(wrapper, [mockFileBar]);
+      await sleep(0);
+      expect(onFail).toHaveBeenCalledTimes(2);
+    });
+
+    it(': sizeLimit', () => {
+      const props = {
+        requestMethod,
+        sizeLimit: {
+          size: 1,
+          unit: 'KB',
+          message: '图片大小不超过 {sizeLimit} KB',
+        },
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      const target = wrapper.findComponent(Upload);
+      triggerUploadFile(wrapper, [
+        new File([new ArrayBuffer(1023)], 'foo.png', {
+          type: 'image/png',
+        }),
+      ]);
+      expect(wrapper.vm.uploadFiles.length).toBe(1);
+
+      target.vm.uploadFiles = [];
+      triggerUploadFile(wrapper, [
+        new File([new ArrayBuffer(1025)], 'foo.png', {
+          type: 'image/png',
+        }),
+      ]);
+      expect(wrapper.vm.uploadFiles.length).toBe(0);
+    });
+  });
+
+  describe('slots', () => {
+    it(': deleteBtn', async () => {
+      const props = {
+        requestMethod,
+        deleteBtn: () => <AddIcon />,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+      await nextTick();
+      expect(wrapper.findComponent(AddIcon).exists()).toBe(true);
+    });
+
+    it(': trigger', async () => {
+      const wrapper = mount(
+        <Upload>
+          <UploadIcon />
+        </Upload>,
+      );
+
+      await nextTick();
+
+      const triggerTarget = wrapper.findComponent(UploadIcon);
+      expect(triggerTarget.exists()).toBe(true);
+
+      const spy = vi.spyOn(wrapper.vm, 'triggerUpload');
+      await triggerTarget.trigger('click');
+      expect(spy).toHaveBeenCalled();
+    });
+  });
+
+  describe('event', () => {
+    it(': events', async () => {
+      const props = {
+        requestMethod,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+
+      await sleep(0);
+      expect(wrapper.emitted()).toHaveProperty('select-change');
+      expect(wrapper.emitted()).toHaveProperty('success');
+
+      await wrapper.find('.t-upload__card-image').trigger('click');
+      expect(wrapper.emitted()).toHaveProperty('preview');
+
+      await wrapper.findComponent(CloseIcon).trigger('click');
+      expect(wrapper.emitted()).toHaveProperty('remove');
+    });
+  });
+
+  describe('function', () => {
+    it(': beforeUpload', async () => {
+      const canUpload = ref(false);
+      const beforeUpload = vi.fn(() => canUpload.value);
+
+      const props = {
+        requestMethod,
+        beforeUpload,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+      await nextTick();
+      expect(beforeUpload).toHaveBeenCalled();
+      expect(beforeUpload).toHaveReturnedWith(false);
+
+      canUpload.value = true;
+      triggerUploadFile(wrapper, [mockFileBar]);
+      await nextTick();
+      expect(beforeUpload).toHaveReturnedWith(true);
+    });
+
+    it(': format', async () => {
+      const format = vi.fn((file) => ({ ...file, name: 'bar.png', raw: file }));
+
+      const props = {
+        requestMethod,
+        format,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileBar]);
+      await nextTick();
+      expect(format).toHaveBeenCalled();
+      expect(wrapper.vm.toUploadFiles[0].name).toBe('bar.png');
+    });
+
+    it(': onFail', async () => {
+      const onFail = vi.fn();
+
+      const props = {
+        requestMethod: () =>
+          new Promise((resolve, reject) =>
+            resolve({
+              status: 'fail',
+              error: 'bar',
+            }),
+          ),
+        onFail,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+
+      await sleep(0);
+      expect(onFail).toHaveBeenCalled();
+
+      const spy = vi.spyOn(wrapper.vm, 'handleReload');
+      await wrapper.findComponent(RefreshIcon).trigger('click');
+      expect(spy).toHaveBeenCalled();
+    });
+
+    it(': on-functions', async () => {
+      const onSelectChange = vi.fn();
+      const onSuccess = vi.fn();
+      const onPreview = vi.fn();
+      const onRemove = vi.fn();
+
+      const props = {
+        requestMethod,
+        onSelectChange,
+        onSuccess,
+        onPreview,
+        onRemove,
+      };
+
+      const wrapper = mount(Upload, {
+        props,
+      });
+
+      triggerUploadFile(wrapper, [mockFileFoo]);
+
+      await sleep(0);
+      expect(onSelectChange).toHaveBeenCalled();
+      expect(onSuccess).toHaveBeenCalled();
+
+      await wrapper.find('.t-upload__card-image').trigger('click');
+      expect(onPreview).toHaveBeenCalled();
+
+      await wrapper.findComponent(CloseIcon).trigger('click');
+      expect(onRemove).toHaveBeenCalled();
+    });
+  });
+});