Repository: Tencent/tdesign-mobile-vue
Pull request #325 (2022-08-29 08:39:26)

Merge branch 'lp-calendar' of https://github.com/LadyChatterleyLover/…
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [ ] 日常 bug 修复
- [x] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue

<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->
https://github.com/Tencent/tdesign-mobile-vue/issues/260
### 💡 需求背景和解决方案
完成Calendar组件
<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->

### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->

- feat(calendar): 新增calendar组件

- [ ] 本条 PR 不需要纳入 Changelog

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [x] 文档已补充或无须补充
- [x] 代码演示已提供或无须提供
- [x] TypeScript 定义已补充或无须补充
- [x] Changelog 已提供或无须提供



Patch:
diff --git a/.eslintignore b/.eslintignore
index d396a879..ee8db377 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,9 +1,7 @@
-**/*test*
 **/*site*
 **/*_common*
 **/*types*
 !.*.js
-
 # build files
 es
 esm
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a865984..e78aff15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,46 @@ 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
+- `官网`: 更新浏览器兼容性说明文档 @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))
+- 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))
diff --git a/getting-started.md b/getting-started.md
deleted file mode 100644
index a074dfa5..00000000
--- a/getting-started.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: Vue3 for Mobile
-description: TDesign 适配移动端的组件库，适合在 vue 3 技术栈项目中使用。
-spline: explain
----
-
-
-
-<div style="background: rgba(0, 168, 112, .1); display: flex; align-items: center; line-height: 20px; padding: 14px 24px; border-radius: 3px; color: #555a65">
-  <svg fill="none" viewBox="0 0 16 16" width="16px" height="16px" style="margin-right: 5px">
-    <path fill="#00a870" d="M8 15A7 7 0 108 1a7 7 0 000 14zM7.4 4h1.2v1.2H7.4V4zm.1 2.5h1V12h-1V6.5z" fillOpacity="0.9"></path>
-  </svg>
-  目前组件库处于 Beta 阶段，快速迭代中，请留意版本变化。
-</div>
-
-## 安装
-
-目前组件库已发布一期组件的测试版本，还在快速迭代，注意留意版本变化
-
-```bash
-npm i tdesign-mobile-vue
-```
-
-## 基础使用
-
-推荐使用 Webpack 或 Rollup 等支持 tree-shaking 特性的构建工具，无需额外配置即可实现组件按需引入：
-
-```js
-import { createApp } from 'vue';
-import TDesign from 'tdesign-mobile-vue';
-
-const app = createApp(App);
-app.use(TDesign);
-```
-
-
-## 浏览器兼容性
-
-| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Safari |
-| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| >= 12                                                                                                                                                                                                  | >= 18                                                                                                                                                                                                   | >= 49                                                                                                                                                                                               | >= 10                                                                                                                                                                                               |
diff --git a/package.json b/package.json
index 93dc33fa..23e00d3c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "tdesign-mobile-vue",
   "purename": "tdesign",
-  "version": "0.10.4",
+  "version": "0.10.6",
   "description": "tdesign-mobile-vue",
   "author": "TDesign Group",
   "homepage": "https://tdesign.tencent.com/mobile-vue/",
@@ -111,7 +111,7 @@
     "@types/jest": "^28.1.6",
     "@types/prismjs": "^1.16.2",
     "@typescript-eslint/eslint-plugin": "^5.5.0",
-    "@typescript-eslint/parser": "^5.5.0",
+    "@typescript-eslint/parser": "~5.34.0",
     "@vitejs/plugin-vue": "^1.2.3",
     "@vitejs/plugin-vue-jsx": "^1.3.3",
     "@vitest/ui": "^0.16.0",
@@ -178,8 +178,9 @@
     }
   },
   "lint-staged": {
-    "*.{vue,ts,tsx}": [
-      "eslint --fix"
+    "*.{vue,ts,tsx,js}": [
+      "eslint --fix",
+      "prettier --write"
     ]
   }
 }
diff --git a/site/docs.config.js b/site/docs.config.js
index 5729f35e..9650ca70 100644
--- a/site/docs.config.js
+++ b/site/docs.config.js
@@ -8,7 +8,7 @@ export default {
           title: '快速开始',
           name: 'getting-started',
           path: '/mobile-vue/getting-started',
-          component: () => import('@/../getting-started.md'),
+          component: () => import('@docs/getting-started.md'),
         },
         {
           title: '更新日志',
@@ -70,7 +70,7 @@ export default {
           path: '/mobile-vue/components/grid',
           component: () => import('@/grid/grid.md'),
         },
-      ]
+      ],
     },
     {
       title: '导航',
@@ -124,6 +124,12 @@ export default {
       title: '输入',
       type: 'component',
       children: [
+        {
+          title: 'Calendar 日历',
+          name: 'calendar',
+          path: '/mobile-vue/components/calendar',
+          component: () => import('@/calendar/calendar.md'),
+        },
         {
           title: 'CheckBox 复选框',
           name: 'checkbox',
diff --git a/site/docs/getting-started.md b/site/docs/getting-started.md
new file mode 100644
index 00000000..89777962
--- /dev/null
+++ b/site/docs/getting-started.md
@@ -0,0 +1,44 @@
+---
+title: Vue3 for Mobile
+description: TDesign 适配移动端的组件库，适合在 vue 3 技术栈项目中使用。
+spline: explain
+---
+
+
+
+<div style="background: rgba(0, 168, 112, .1); display: flex; align-items: center; line-height: 20px; padding: 14px 24px; border-radius: 3px; color: #555a65">
+  <svg fill="none" viewBox="0 0 16 16" width="16px" height="16px" style="margin-right: 5px">
+    <path fill="#00a870" d="M8 15A7 7 0 108 1a7 7 0 000 14zM7.4 4h1.2v1.2H7.4V4zm.1 2.5h1V12h-1V6.5z" fillOpacity="0.9"></path>
+  </svg>
+  目前组件库处于 Beta 阶段，快速迭代中，请留意版本变化。
+</div>
+
+## 安装
+
+目前组件库已发布一期组件的测试版本，还在快速迭代，注意留意版本变化
+
+```bash
+npm i tdesign-mobile-vue
+```
+
+## 基础使用
+
+推荐使用 Webpack 或 Rollup 等支持 tree-shaking 特性的构建工具，无需额外配置即可实现组件按需引入：
+
+```js
+import { createApp } from 'vue';
+import TDesign from 'tdesign-mobile-vue';
+
+const app = createApp(App);
+app.use(TDesign);
+```
+
+## 浏览器兼容性
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari-ios/safari-ios_48x48.png" alt="iOS Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/> iOS Safari| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/samsung-internet/samsung-internet_48x48.png" alt="Samsung" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Samsung | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br/>Opera |[<img src="https://user-images.githubusercontent.com/51158141/189169679-71e045f6-9b9b-4baf-8b9f-e045a40216f5.png" alt="Android Browser" width="24px" height="24px" />]()<br/>Android Browser|
+| --------- | --------- | --------- | --------- | --------- |--------- |
+| Firefox >=104| Chrome >=105| iOS Safari >=12.2| Samsung >=10.2 | Opera >=64 | Android Browser >=105 |
+
+详情参见[移动端组件库浏览器兼容性说明](https://github.com/Tencent/tdesign/wiki/Browser-Compatibility)
+
+
diff --git a/site/vite.config.js b/site/vite.config.js
index 6a2f183f..7d611c45 100644
--- a/site/vite.config.js
+++ b/site/vite.config.js
@@ -4,6 +4,8 @@ import vue from '@vitejs/plugin-vue';
 import { defineConfig } from 'vite';
 import vueJsx from '@vitejs/plugin-vue-jsx';
 
+const resolvePath = (r) => path.resolve(__dirname, r);
+
 const publicPathMap = {
   preview: '/',
   intranet: '/mobile-vue/',
@@ -47,9 +49,10 @@ export default ({ mode }) => {
     root: '.',
     resolve: {
       alias: {
-        '@': path.resolve(__dirname, '../src'),
-        'tdesign-mobile-vue': path.resolve(__dirname, '../src'),
-        '@common': path.resolve(__dirname, '../src/_common'),
+        '@': resolvePath('../src'),
+        '@docs': resolvePath('./docs'),
+        '@common': resolvePath('../src/_common'),
+        'tdesign-mobile-vue': resolvePath('../src'),
       },
     },
     server: {
@@ -76,7 +79,7 @@ export default ({ mode }) => {
         },
       }),
       vueJsx({
-        isCustomElement
+        isCustomElement,
       }),
       createTDesignPlugin(),
     ],
diff --git a/src/_common b/src/_common
index 51955cca..46125ae7 160000
--- a/src/_common
+++ b/src/_common
@@ -1,1 +1,1 @@
-Subproject commit 51955ccaf67c8498080ea9fdd628af4b0421b717
+Subproject commit 46125ae7b1fe2b18e3bf1cc1d15a04e802a4b06e
diff --git a/src/action-sheet/action-sheet.md b/src/action-sheet/action-sheet.md
index 5425e5e8..4b490209 100644
--- a/src/action-sheet/action-sheet.md
+++ b/src/action-sheet/action-sheet.md
@@ -1,10 +1,12 @@
 :: BASE_DOC ::
 
 ## API
+
 ### ActionSheet Props
 
 名称 | 类型 | 默认值 | 说明 | 必传
 -- | -- | -- | -- | --
+type | string | list | 展示类型，可选项：list/grid | N
 cancelText | String | 取消 | 设置取消按钮的文本 | N
 count | Number | 8 | 设置每页展示菜单的数量，仅当 type=grid 时有效 | N
 items | Array | - | 必需。菜单项。TS 类型：`Array<string | ActionSheetItem>` `interface ActionSheetItem {label: string; color?: string; disabled?: boolean }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/action-sheet/type.ts) | Y
diff --git a/src/action-sheet/menu-grid.vue b/src/action-sheet/menu-grid.vue
index 6d7b6541..000bbcd4 100644
--- a/src/action-sheet/menu-grid.vue
+++ b/src/action-sheet/menu-grid.vue
@@ -9,7 +9,13 @@
     >
       <div v-for="(Items, i) in actionItems" :key="i" :class="`${name}__menu`">
         <t-grid :column="gridColumn">
-          <t-grid-item v-for="(item, index) in Items" :key="index" :text="item.label" :image="item.icon" />
+          <t-grid-item
+            v-for="(item, index) in Items"
+            :key="index"
+            :text="item.label"
+            :image="item.icon"
+            @click="handleSelected(i * count + index)"
+          />
         </t-grid>
       </div>
     </div>
diff --git a/src/back-top/back-top.vue b/src/back-top/back-top.vue
index 053c5e4a..1d04b0de 100644
--- a/src/back-top/back-top.vue
+++ b/src/back-top/back-top.vue
@@ -45,7 +45,7 @@ export default defineComponent({
 
     const clickBackBtn = () => {
       window.document.documentElement.scrollTop += top.value;
-      emitEvent('toTop');
+      emitEvent('to-top');
     };
 
     return {
diff --git a/src/calendar/calendar.md b/src/calendar/calendar.md
new file mode 100644
index 00000000..c26b7d49
--- /dev/null
+++ b/src/calendar/calendar.md
@@ -0,0 +1,21 @@
+:: BASE_DOC ::
+
+## API
+
+### 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
+
+### 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;}` | 点击确认按钮触发 |
diff --git a/src/calendar/calendar.vue b/src/calendar/calendar.vue
new file mode 100644
index 00000000..c8e139c1
--- /dev/null
+++ b/src/calendar/calendar.vue
@@ -0,0 +1,240 @@
+<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>
+        </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>
+        </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>
+          </div>
+        </div>
+        <div class="t-calendar__btn" @click="confirm">
+          <t-button block :style="{ background: bgColor, color: '#fff' }">{{ confirmBtn }}</t-button>
+        </div>
+      </div>
+    </t-popup>
+  </div>
+</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 config from '../config';
+import type { CalendarCell, CalendarValue } from './type';
+
+const { prefix } = config;
+const name = `${prefix}-calendar`;
+
+export default defineComponent({
+  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));
+      }
+      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),
+      };
+    });
+    return {
+      name,
+      ...toRefs(props),
+      getYearMonthDay,
+      flag,
+      value,
+      days,
+      time,
+      dateValue,
+      close,
+      getDate,
+      isCurrentMonth,
+      isToday,
+      preYear,
+      nextYear,
+      preMonth,
+      nextMonth,
+      chooseDay,
+      confirm,
+      formatDate,
+      showDays,
+    };
+  },
+});
+</script>
diff --git a/src/calendar/demos/base.vue b/src/calendar/demos/base.vue
new file mode 100644
index 00000000..d5da19b6
--- /dev/null
+++ b/src/calendar/demos/base.vue
@@ -0,0 +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>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { defineComponent, ref } from 'vue';
+
+const visible = ref(false);
+const handleConfirm = (val: string) => {
+  console.log(val);
+};
+const cellClick = (val: any) => {
+  console.log(val);
+};
+</script>
diff --git a/src/calendar/demos/cell.vue b/src/calendar/demos/cell.vue
new file mode 100644
index 00000000..f7c48a94
--- /dev/null
+++ b/src/calendar/demos/cell.vue
@@ -0,0 +1,19 @@
+<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
new file mode 100644
index 00000000..142fd677
--- /dev/null
+++ b/src/calendar/demos/color.vue
@@ -0,0 +1,12 @@
+<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/head.vue b/src/calendar/demos/head.vue
new file mode 100644
index 00000000..0ee0a6aa
--- /dev/null
+++ b/src/calendar/demos/head.vue
@@ -0,0 +1,14 @@
+<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
new file mode 100644
index 00000000..78fefaea
--- /dev/null
+++ b/src/calendar/demos/mobile.vue
@@ -0,0 +1,43 @@
+<template>
+  <div class="tdesign-mobile-demo">
+    <h1 class="title">Calendar 日历</h1>
+    <p class="summary">按钮用于选择日期。</p>
+
+    <tdesign-demo-block title="01 基础" summary="基础用法">
+      <div class="Calendar-demo">
+        <BaseCalendarDemo />
+      </div>
+    </tdesign-demo-block>
+
+    <tdesign-demo-block title="02 默认日期" summary="设置默认日期">
+      <div class="Calendar-demo">
+        <ValueCalendarDemo />
+      </div>
+    </tdesign-demo-block>
+
+    <tdesign-demo-block title="03 设置标题" summary="自定义标题">
+      <div class="Calendar-demo">
+        <HeadCalendarDemo />
+      </div>
+    </tdesign-demo-block>
+
+    <tdesign-demo-block title="04 设置单元格" summary="自定义单元格">
+      <div class="Calendar-demo">
+        <CellCalendarDemo />
+      </div>
+    </tdesign-demo-block>
+
+    <tdesign-demo-block title="04 设置背景颜色" summary="自定义单元格和按钮颜色">
+      <div class="Calendar-demo">
+        <ColorCalendarDemo />
+      </div>
+    </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';
+</script>
diff --git a/src/calendar/demos/value.vue b/src/calendar/demos/value.vue
new file mode 100644
index 00000000..82db1a79
--- /dev/null
+++ b/src/calendar/demos/value.vue
@@ -0,0 +1,13 @@
+<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/index.ts b/src/calendar/index.ts
new file mode 100644
index 00000000..c35637d7
--- /dev/null
+++ b/src/calendar/index.ts
@@ -0,0 +1,11 @@
+import Calendar from './calendar.vue';
+import { withInstall, WithInstallType } from '../shared';
+
+import './style';
+import { TdCalendarProps } from './type';
+
+export * from './type';
+export type CalendarProps = TdCalendarProps;
+
+const _Calendar: WithInstallType<typeof Calendar> = withInstall(Calendar);
+export default _Calendar;
diff --git a/src/calendar/props.ts b/src/calendar/props.ts
new file mode 100644
index 00000000..760cf3aa
--- /dev/null
+++ b/src/calendar/props.ts
@@ -0,0 +1,48 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件，请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TdCalendarProps } from './type';
+import { PropType } from 'vue';
+
+export default {
+  /**
+   * 控制日历显示隐藏
+   */
+  modelValue: {
+    type: Boolean,
+  },
+  // 按钮颜色
+  bgColor: {
+    type: String,
+    default: '#0053DB',
+  },
+  /**
+   * 确认按钮
+   */
+  confirmBtn: {
+    type: String,
+    default: '确认',
+  },
+  /**
+   * 第一天从星期几开始，仅在日历展示维度为月份时（mode = month）有效。默认为 1
+   */
+  firstDayOfWeek: {
+    type: Number,
+  },
+  /**
+   * 头部插槽（左上角处，默认不显示任何内容）
+   */
+  head: {
+    type: String,
+  },
+  /**
+   * 当前高亮的日期
+   */
+  value: {
+    type:  Date as unknown as PropType<TdCalendarProps['value']>,
+    default: new Date(),
+  },
+};
diff --git a/src/calendar/style/css.js b/src/calendar/style/css.js
new file mode 100644
index 00000000..6a9a4b13
--- /dev/null
+++ b/src/calendar/style/css.js
@@ -0,0 +1,1 @@
+import './index.css';
diff --git a/src/calendar/style/index.js b/src/calendar/style/index.js
new file mode 100644
index 00000000..ccfc1c7f
--- /dev/null
+++ b/src/calendar/style/index.js
@@ -0,0 +1,1 @@
+import '../../_common/style/mobile/components/calendar/_index.less';
diff --git a/src/calendar/type.ts b/src/calendar/type.ts
new file mode 100644
index 00000000..ccc9b0c0
--- /dev/null
+++ b/src/calendar/type.ts
@@ -0,0 +1,52 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件，请勿随意修改。如需修改请联系 PMC
+ * */
+export interface TdCalendarProps {
+  /**
+   * 控制日历显示隐藏
+   */
+  modelValue: boolean;
+  /**
+   * 背景颜色
+   */
+  bgColor?: string;
+
+  /**
+   * 确认按钮
+   */
+  confirmBtn?: string;
+  /**
+   * 第一天从星期几开始，仅在日历展示维度为月份时（mode = month）有效。默认为 1
+   */
+  firstDayOfWeek?: number;
+  /**
+   * 头部插槽（左上角处，默认不显示任何内容）
+   */
+  head?: string;
+  /**
+   * 当前高亮的日期
+   */
+  value?: Date;
+}
+
+export interface DateCellDescription {
+  date: Date;
+  label: string;
+}
+
+export type CalendarCell = {
+  year: number;
+  month: number;
+  day: number;
+}
+
+export type CalendarValue = {
+  year: number;
+  month: number;
+  day: number;
+  week: number;
+  date: string;
+  isToday: boolean;
+};
\ No newline at end of file
diff --git a/src/collapse/collapse-panel.vue b/src/collapse/collapse-panel.vue
index abe4417d..54f0ea82 100644
--- a/src/collapse/collapse-panel.vue
+++ b/src/collapse/collapse-panel.vue
@@ -5,7 +5,7 @@
         <slot name="header">{{ header }}</slot>
       </div>
       <div :class="`${classPrefix}__header-right`">
-        <div v-if="headerRightContent || $slots.extra" :class="`${classPrefix}__header-extra`">
+        <div v-if="headerRightContent || $slots.headerRightContent" :class="`${classPrefix}__header-extra`">
           <slot name="headerRightContent">{{ headerRightContent }}</slot>
         </div>
         <component :is="rightIcon" :class="`${classPrefix}__header-icon`" />
diff --git a/src/components.ts b/src/components.ts
index 8d41ce71..89a39f39 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -1,6 +1,7 @@
 export { default as Button } from './button';
 export { default as ButtonGroup } from './button-group';
 export { default as Divider } from './divider';
+export { default as Calendar } from './calendar';
 export { default as Cell } from './cell';
 export { default as CellGroup } from './cell-group';
 export { default as Checkbox, CheckboxGroup } from './checkbox';
diff --git a/src/count-down/count-down.vue b/src/count-down/count-down.vue
index 1891565b..7bab8bb9 100644
--- a/src/count-down/count-down.vue
+++ b/src/count-down/count-down.vue
@@ -29,11 +29,9 @@ export default defineComponent({
     ...CountDownProps,
   },
   setup(props) {
-    const { content, size = 'small', theme = 'default', ...other } = props || {};
-    //
-    const { time, showTimes } = useCountDown(other);
+    const { time, showTimes } = useCountDown(props);
     const internalInstance = getCurrentInstance();
-    const hasChinese = /.*[\u4e00-\u9fa5]+.*$/?.test?.(other?.format);
+    const hasChinese = /.*[\u4e00-\u9fa5]+.*$/?.test?.(props?.format);
     const contentLayout = computed(() => renderTNode(internalInstance, 'content'));
     // return
     return {
diff --git a/src/date-time-picker/date-time-picker.vue b/src/date-time-picker/date-time-picker.vue
index 18a6bae0..28b47b31 100644
--- a/src/date-time-picker/date-time-picker.vue
+++ b/src/date-time-picker/date-time-picker.vue
@@ -1,6 +1,7 @@
 <template>
   <div :class="className">
     <t-picker
+      ref="pickeInstance"
       :value="currentPicker"
       :title="title"
       :columns="(selected) => generateDatePickerColumns(selected, start, end, renderLabel)"
@@ -14,7 +15,7 @@
 </template>
 
 <script lang="ts">
-import { ref, computed, defineComponent, SetupContext, toRefs } from 'vue';
+import { ref, computed, defineComponent, SetupContext, toRefs, watch, nextTick } from 'vue';
 import dayjs from 'dayjs';
 import weekday from 'dayjs/plugin/weekday';
 import customParseFormat from 'dayjs/plugin/customParseFormat';
@@ -43,10 +44,12 @@ export default defineComponent({
   name,
   components: { TPicker },
   props: DateTimePickerProps,
-  emits: ['change', 'cancel', 'confirm', 'pick'],
+  emits: ['change', 'cancel', 'confirm', 'pick', 'update:modelValue', 'update:value'],
   setup(props: any, context: SetupContext) {
     const emitEvent = useEmitEvent(props, context.emit);
-
+    const pickeInstance = ref<any>(null);
+    const isChanged = ref(false);
+    const realDateValue = ref();
     const className = computed(() => [`${name}`]);
     const { value, modelValue } = toRefs(props);
     const [dateTimePickerValue, setDateTimePickerValue] = useVModel(
@@ -127,7 +130,7 @@ export default defineComponent({
     };
 
     // 当默认v-model为空时，当前value取最小日期
-    const pickerValue = ref(getPickerValueByDateTimePickerValue(dateTimePickerValue.value || start.value));
+    const pickerValue = ref(getPickerValueByDateTimePickerValue(dateTimePickerValue.value || start.value.valueOf()));
 
     let lastTimePicker = [...pickerValue.value];
     let currentPicker = [...pickerValue.value];
@@ -215,7 +218,7 @@ export default defineComponent({
 
       if (isPrecision('date')) {
         const lower = isInMinMonth ? minDay : 1;
-        const upper = isInMaxMonth ? maxDay : dayjs(`${selected[0]}-${selected[1]}`).daysInMonth();
+        const upper = isInMaxMonth ? maxDay : dayjs(`${selected[0]}-${+selected[1] + 1}`).daysInMonth();
         generateColumn(lower, upper, 'date');
       }
 
@@ -253,8 +256,8 @@ export default defineComponent({
     const onChange = (value: Array<PickerValue>, context: { columns: Array<PickerContext>; e: MouseEvent }) => {
       lastTimePicker = [...currentPicker];
       const currentDate = getDateTimePickerValueByPickerValue(value);
-      const realDateValue = dayjs(currentDate).format(props.format);
-      setDateTimePickerValue(realDateValue);
+      realDateValue.value = dayjs(currentDate).format(props.format);
+      isChanged.value = true;
     };
 
     const onPick = (value: Array<PickerValue>, context: PickerContext) => {
@@ -263,7 +266,46 @@ export default defineComponent({
       emitEvent('pick', dayjs(currentDate).format(props.format));
     };
 
+    /**
+     * 监听v-model变化，当modelValue发生变化时，更新数据和ui
+     * isChanged用来区分用户直接修改v-model，和滑动picker修改value两种方式
+     * 只有用户滑动picker时，isChanged为true，滑动结束后设为false
+     */
+    watch(
+      () => dateTimePickerValue,
+      (val) => {
+        nextTick(() => {
+          if (isChanged.value) {
+            isChanged.value = false;
+          } else {
+            pickeInstance.value?.setValues(getPickerValueByDateTimePickerValue(val.value || start.value.valueOf()));
+            currentPicker = [...ref(getPickerValueByDateTimePickerValue(val.value || start.value.valueOf())).value];
+            lastTimePicker = [...currentPicker];
+            isChanged.value = false;
+          }
+        });
+      },
+      {
+        immediate: true,
+        deep: true,
+      },
+    );
+
+    watch(
+      () => isChanged,
+      (val) => {
+        if (val.value) {
+          setDateTimePickerValue(realDateValue.value);
+        }
+      },
+      {
+        immediate: true,
+        deep: true,
+      },
+    );
+
     return {
+      pickeInstance,
       className,
       confirmButtonText,
       cancelButtonText,
@@ -273,6 +315,8 @@ export default defineComponent({
       renderLabel,
       pickerValue,
       currentPicker,
+      realDateValue,
+      isChanged,
       generateDatePickerColumns,
       onConfirm,
       onCancel,
diff --git a/src/date-time-picker/demos/base.vue b/src/date-time-picker/demos/base.vue
index daaa7518..09b3120c 100644
--- a/src/date-time-picker/demos/base.vue
+++ b/src/date-time-picker/demos/base.vue
@@ -1,7 +1,7 @@
 <template>
   <tdesign-demo-block>
     <t-input :value="text.ymdhms" label="选择日期时间" placeholder="日期时间" @click="show.ymdhms = true" />
-    <t-popup v-model="show.ymdhms" position="bottom">
+    <t-popup v-model="show.ymdhms" placement="bottom">
       <t-date-time-picker
         v-model="text.ymdhms"
         :mode="['date', 'second']"
diff --git a/src/date-time-picker/demos/hour-minute.vue b/src/date-time-picker/demos/hour-minute.vue
index aae927ee..adde09be 100644
--- a/src/date-time-picker/demos/hour-minute.vue
+++ b/src/date-time-picker/demos/hour-minute.vue
@@ -1,7 +1,7 @@
 <template>
   <tdesign-demo-block>
     <t-input :value="text.hm" label="选择时分" placeholder="时分" @click="show.hm = true" />
-    <t-popup v-model="show.hm" position="bottom">
+    <t-popup v-model="show.hm" placement="bottom">
       <t-date-time-picker
         v-model="text.hm"
         :mode="[null, 'minute']"
diff --git a/src/date-time-picker/demos/render-label-date.vue b/src/date-time-picker/demos/render-label-date.vue
index 5f2fea44..cb2a92cc 100644
--- a/src/date-time-picker/demos/render-label-date.vue
+++ b/src/date-time-picker/demos/render-label-date.vue
@@ -1,7 +1,7 @@
 <template>
   <tdesign-demo-block>
     <t-input :value="text.ym" label="选择年月" placeholder="年月" @click="show.ym = true" />
-    <t-popup v-model="show.ym" position="bottom">
+    <t-popup v-model="show.ym" placement="bottom">
       <t-date-time-picker
         v-model="text.ym"
         :mode="mode"
diff --git a/src/date-time-picker/demos/start-end-date.vue b/src/date-time-picker/demos/start-end-date.vue
index e369266e..7a49976a 100644
--- a/src/date-time-picker/demos/start-end-date.vue
+++ b/src/date-time-picker/demos/start-end-date.vue
@@ -1,7 +1,7 @@
 <template>
   <tdesign-demo-block>
     <t-input :value="text.ymd" label="选择年月日" placeholder="年月日" @click="show.ymd = true" />
-    <t-popup v-model="show.ymd" position="bottom">
+    <t-popup v-model="show.ymd" placement="bottom">
       <t-date-time-picker
         v-model="text.ymd"
         :mode="mode"
diff --git a/src/dialog/dialog.vue b/src/dialog/dialog.vue
index 775b3379..e03e0356 100644
--- a/src/dialog/dialog.vue
+++ b/src/dialog/dialog.vue
@@ -63,7 +63,8 @@ export default defineComponent({
   emits: ['update:visible', 'confirm', 'overlay-click', 'cancel', 'change', 'close'],
   setup(props, context) {
     const internalInstance = getCurrentInstance();
-    const dialogContent = renderContent(internalInstance, 'default', 'content');
+    const dialogContent = computed(() => renderContent(internalInstance, 'default', 'content'));
+
     const innerValue = ref('');
     const dClassName = computed(() => `${name}`);
     const dBoxClassName = computed(() => `${name}__box`);
diff --git a/src/image-viewer/demos/mobile.vue b/src/image-viewer/demos/mobile.vue
index afe0ca10..fa461a06 100644
--- a/src/image-viewer/demos/mobile.vue
+++ b/src/image-viewer/demos/mobile.vue
@@ -16,7 +16,6 @@
 </template>
 <script lang="ts">
 import { ref, defineComponent, Ref } from 'vue';
-import { TdImageViewerProps } from '../type';
 
 export default defineComponent({
   setup() {
diff --git a/src/image-viewer/image-viewer.vue b/src/image-viewer/image-viewer.vue
index 2abe8b07..4b1f15a2 100644
--- a/src/image-viewer/image-viewer.vue
+++ b/src/image-viewer/image-viewer.vue
@@ -35,8 +35,6 @@ import {
   defineComponent,
   reactive,
   watch,
-  nextTick,
-  PropType,
   getCurrentInstance,
   CSSProperties,
   SetupContext,
@@ -46,7 +44,8 @@ import config from '../config';
 import ImageViewerProps from './props';
 import { renderTNode, TNode, useEmitEvent, useDefault, useTouch } from '../shared';
 import { TdImageViewerProps } from './type';
-import { Toast } from '..';
+import { Swiper as TSwiper, SwiperItem as TSwiperItem, SwiperNavigation } from '../swiper';
+import TOverlay from '../overlay';
 
 export type TriggerType = 'close-btn' | 'overlay' | 'esc';
 const { prefix } = config;
@@ -65,6 +64,9 @@ export default defineComponent({
   name,
   components: {
     CloseCircleFilledIcon,
+    TSwiper,
+    TSwiperItem,
+    TOverlay,
     TNode,
   },
   props: ImageViewerProps,
@@ -88,7 +90,7 @@ export default defineComponent({
     const closeBtnTNode = computed(() => {
       return renderTNode(internalInstance, 'closeBtn');
     });
-    const navigation = computed(() => {
+    const navigation = computed<SwiperNavigation>(() => {
       if (props.showIndex) {
         return { type: 'fraction' };
       }
@@ -126,7 +128,6 @@ export default defineComponent({
       event.preventDefault();
       event.stopPropagation();
       const { touches } = event;
-      const { offsetX } = touch;
 
       touch.start(event);
 
diff --git a/src/list/demos/mobile.vue b/src/list/demos/mobile.vue
index fc0dd0e1..c846579b 100644
--- a/src/list/demos/mobile.vue
+++ b/src/list/demos/mobile.vue
@@ -52,7 +52,7 @@ import { defineComponent, ref, onMounted, computed, h, onUnmounted } from 'vue';
 const MAX_DATA_LEN = 60;
 
 const loadData = (data: any, isRefresh?: Boolean) => {
-  const ONCE_LOAD_NUM = 15;
+  const ONCE_LOAD_NUM = 20;
 
   return new Promise((resolve) => {
     setTimeout(() => {
@@ -124,7 +124,7 @@ export default defineComponent({
     };
 
     const onScroll = (scrollBottom: number, type?: string) => {
-      if (scrollBottom === 0) {
+      if (scrollBottom < 50) {
         type === 'listPull' ? onLoadPull() : onLoad();
       }
     };
diff --git a/src/picker/picker-item.vue b/src/picker/picker-item.vue
index 90177892..a182c941 100644
--- a/src/picker/picker-item.vue
+++ b/src/picker/picker-item.vue
@@ -51,18 +51,32 @@ export default defineComponent({
     const wrapperClassName = computed(() => [`${name}__wrapper`]);
     const itemClassName = computed(() => [`${name}__item`]);
     const setIndex = (index: number) => {
-      picker && picker.updateIndex(index);
+      if (picker) {
+        picker.updateItems();
+        picker.updateIndex(index, {
+          isChange: false,
+        });
+      }
     };
     const setValue = (value: number | string | undefined) => {
-      picker && picker.updateIndex(getIndexByValue(value));
+      if (picker) {
+        picker.updateItems();
+        picker.updateIndex(getIndexByValue(value), {
+          isChange: false,
+        });
+      }
     };
     const setOptions = () => {
       picker && picker.update();
     };
+    const setUpdateItems = () => {
+      picker && picker.updateItems();
+    };
     context.expose({
       setIndex,
       setValue,
       setOptions,
+      setUpdateItems,
     });
 
     onMounted(() => {
diff --git a/src/picker/picker.class.ts b/src/picker/picker.class.ts
index a602e1e0..3b268485 100644
--- a/src/picker/picker.class.ts
+++ b/src/picker/picker.class.ts
@@ -241,16 +241,21 @@ class Picker {
    * @param index
    * @param duration
    */
-  updateIndex(index: number, duration = 460): void {
+  updateIndex(index: number, options?: any): void {
+    const realOptions = {
+      duration: 460,
+      isChange: true,
+      ...options,
+    };
     this.curIndex = index;
     this.setSelectedClassName();
     const moveOffsetY = (-index + 2) * this.itemHeight;
     if (this.list) {
       this.list.style.transform = `translate(0,${moveOffsetY}px) translateZ(0)`;
-      this.list.style.transitionDuration = `${duration}ms`;
+      this.list.style.transitionDuration = `${realOptions.duration}ms`;
       this.list.style.transitionTimingFunction = 'ease-out';
     }
-    this.onChange(index);
+    realOptions.isChange && this.onChange(index);
   }
 
   /**
diff --git a/src/picker/picker.vue b/src/picker/picker.vue
index 5a3f2366..f7d291d3 100644
--- a/src/picker/picker.vue
+++ b/src/picker/picker.vue
@@ -28,7 +28,7 @@ import config from '../config';
 import PickerProps from './props';
 import { PickerValue, PickerColumn, PickerColumnItem } from './type';
 import TButton from '../button';
-import { useEmitEvent, useVModel, useChildSlots, TNode } from '../shared';
+import { useEmitEvent, useVModel, useChildSlots, useExpose } from '../shared';
 import PickerItem from './picker-item.vue';
 
 const { prefix } = config;
@@ -47,7 +47,7 @@ export default defineComponent({
   name,
   components: { TButton, PickerItem },
   props: PickerProps,
-  emits: ['change', 'cancel', 'pick', 'update:modelValue'],
+  emits: ['change', 'cancel', 'pick', 'update:modelValue', 'update:value'],
   setup(props: any, context: SetupContext) {
     const emitEvent = useEmitEvent(props, context.emit);
     const { value, modelValue } = toRefs(props);
@@ -76,7 +76,7 @@ export default defineComponent({
       // 点击确认后，更新最近一次的picker状态
       lastTimeValueArray = [...curValueArray.value];
       lastTimeIndexArray = [...curIndexArray];
-      setPickerValue(curValueArray.value, { index: curIndexArray });
+      setPickerValue(curValueArray.value);
       emitEvent('confirm', curValueArray.value, { index: curIndexArray });
     };
     const handleCancel = (e: MouseEvent) => {
@@ -102,6 +102,10 @@ export default defineComponent({
               nextTick(() => {
                 pickerItemInstanceArray.value[index]?.exposed?.setIndex(0);
               });
+            } else {
+              nextTick(() => {
+                pickerItemInstanceArray.value[index]?.exposed?.setUpdateItems();
+              });
             }
           });
         }
@@ -109,6 +113,21 @@ export default defineComponent({
       }
     };
 
+    const setValues = (values: string[]) => {
+      curValueArray.value = values;
+      setPickerValue(values);
+      // 等columns更新完后，再更新value
+      nextTick(() => {
+        pickerItemInstanceArray.value.forEach((item: any, index: number) => {
+          item.exposed?.setValue(values[index]);
+        });
+      });
+    };
+
+    useExpose({
+      setValues,
+    });
+
     return {
       name,
       pickerValue,
diff --git a/src/search/search.vue b/src/search/search.vue
index 9194455a..ac9db97d 100644
--- a/src/search/search.vue
+++ b/src/search/search.vue
@@ -16,7 +16,6 @@
           :placeholder="placeholder"
           :clearable="clearable"
           @blur="onBlur"
-          @clear="onClear"
           @focus="onFocus"
           @change="onChange"
         />
@@ -94,21 +93,16 @@ export default defineComponent({
 
     const onFocus = (value: InputValue, context: InputFocusContent) => {
       state.labelActive = false;
-      props.onFocus?.(value, { e: context.e });
+      props.onFocus?.(value as string, { e: context.e });
     };
 
     const onCancel = (e: MouseEvent) => {
       state.labelActive = !state.labelActive;
-      props.onCancel?.(e);
       props.onActionClick?.({ e });
     };
 
-    const onClear = (e: Event) => {
-      props.onClean?.(e);
-    };
-
     const onChange = (value: InputValue, context: InputChangeContext) => {
-      props.onChange?.(value, { e: context?.e });
+      props.onChange?.(value as string, { e: context?.e });
     };
 
     extendAPI({ doFocus, blur });
@@ -119,7 +113,6 @@ export default defineComponent({
       classes,
       onClick,
       onCancel,
-      onClear,
       onBlur,
       onFocus,
       onChange,
diff --git a/src/shared/index.ts b/src/shared/index.ts
index 9d8f2346..bd664885 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -14,3 +14,4 @@ export * from './useChildSlots';
 export * from './useVModel';
 export * from './useTouch';
 export * from './useScrollParent';
+export * from './useExpose';
diff --git a/src/shared/useCountDown/index.ts b/src/shared/useCountDown/index.ts
index 5aea3542..ef4d2472 100644
--- a/src/shared/useCountDown/index.ts
+++ b/src/shared/useCountDown/index.ts
@@ -8,7 +8,7 @@ export function useCountDown(props: TdUseCountDownProps): TdUseCountDown {
   // state
   const fps = ref();
   const count = ref(Number(time));
-  const showTimes = reactive(getShowTimes(getRemainTimes(time), format));
+  const showTimes = reactive(getShowTimes(getRemainTimes(time), format, !!millisecond));
 
   // raf
   const { pause, resume } = useRafFn(
diff --git a/src/shared/useCountDown/utils.ts b/src/shared/useCountDown/utils.ts
index 5361bf8a..a2084aef 100644
--- a/src/shared/useCountDown/utils.ts
+++ b/src/shared/useCountDown/utils.ts
@@ -30,7 +30,17 @@ export const getRemainTimes = (time?: number): TimeData => {
  * @param num
  * @returns
  */
-export const fillZero = (num: number): string | number => (num >= 10 ? num : `0${num}`);
+export const fillZero = (num: number, isMillieconds = false): string | number => {
+  if (isMillieconds) {
+    if (num >= 100) {
+      return num;
+    }
+
+    return num >= 10 ? `0${num}` : `00${num}`;
+  }
+
+  return num >= 10 ? num : `0${num}`;
+};
 
 /**
  * getMark
@@ -45,8 +55,9 @@ export const getMark = (format: string, type: string): string => format?.split?.
  * @param time
  * @returns
  */
-export const getShowTimes = (times: TimeData, format: string): TdUseCountDownShowTimes => {
+export const getShowTimes = (times: TimeData, format: string, milliseconds = false): TdUseCountDownShowTimes => {
   format = (format || 'DD:HH:mm:ss')?.toUpperCase?.();
+  milliseconds && !format.includes(':SSS') && (format = format.concat(':SSS'));
   const showTimes: TdUseCountDownShowTimes = [];
   if (format?.indexOf('DD') > -1) {
     showTimes?.push({
@@ -75,7 +86,7 @@ export const getShowTimes = (times: TimeData, format: string): TdUseCountDownSho
   if (format?.indexOf('SSS') > -1) {
     showTimes?.push({
       mark: getMark(format, 'SSS'),
-      value: fillZero(times?.milliseconds),
+      value: fillZero(times?.milliseconds, true),
     });
   }
 
diff --git a/src/shared/useExpose/index.ts b/src/shared/useExpose/index.ts
new file mode 100644
index 00000000..e3cd91bf
--- /dev/null
+++ b/src/shared/useExpose/index.ts
@@ -0,0 +1,9 @@
+import { getCurrentInstance } from 'vue';
+
+// expose public api
+export function useExpose<T = Record<string, any>>(apis: T) {
+  const instance = getCurrentInstance();
+  if (instance) {
+    Object.assign(instance.proxy as object, apis);
+  }
+}
diff --git a/src/stepper/demos/base.vue b/src/stepper/demos/base.vue
index c4a40184..5229f6f7 100644
--- a/src/stepper/demos/base.vue
+++ b/src/stepper/demos/base.vue
@@ -1,21 +1,13 @@
 <template>
-  <div class="cell-base">
-    <t-cell-group title="基础步进器">
-      <t-cell title="标题文字">
-        <t-stepper v-model.number="number" :step="1" :min="0" :max="100" />
-      </t-cell>
-    </t-cell-group>
+  <div class="stepper-demo">
+    <t-cell title="标题文字">
+      <t-stepper v-model="number" />
+    </t-cell>
   </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, ref } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
 
-export default defineComponent({
-  setup() {
-    return {
-      number: ref(10),
-    };
-  },
-});
+const number = ref(0);
 </script>
diff --git a/src/stepper/demos/disable-input.vue b/src/stepper/demos/disable-input.vue
index 2a0c735a..7ab5a8e5 100644
--- a/src/stepper/demos/disable-input.vue
+++ b/src/stepper/demos/disable-input.vue
@@ -1,21 +1,13 @@
 <template>
-  <div class="cell-base">
-    <t-cell-group title="禁止输入">
-      <t-cell title="禁止输入">
-        <t-stepper v-model.number="number" :step="1" :min="0" :max="100" disable-input />
-      </t-cell>
-    </t-cell-group>
+  <div class="stepper-demo">
+    <t-cell title="禁止输入">
+      <t-stepper v-model="number" :step="1" :min="0" :max="100" disable-input />
+    </t-cell>
   </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, ref } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
 
-export default defineComponent({
-  setup() {
-    return {
-      number: ref(10),
-    };
-  },
-});
+const number = ref(0);
 </script>
diff --git a/src/stepper/demos/disabled.vue b/src/stepper/demos/disabled.vue
index bc6a9a76..5488c73d 100644
--- a/src/stepper/demos/disabled.vue
+++ b/src/stepper/demos/disabled.vue
@@ -1,21 +1,13 @@
 <template>
-  <div class="cell-base">
-    <t-cell-group title="禁用状态">
-      <t-cell title="失效">
-        <t-stepper v-model.number="number" :step="1" :min="0" :max="100" disabled />
-      </t-cell>
-    </t-cell-group>
+  <div class="stepper-demo">
+    <t-cell title="禁用">
+      <t-stepper v-model="number" :step="1" :min="0" :max="100" disabled />
+    </t-cell>
   </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, ref } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
 
-export default defineComponent({
-  setup() {
-    return {
-      number: ref(0),
-    };
-  },
-});
+const number = ref(0);
 </script>
diff --git a/src/stepper/demos/event.vue b/src/stepper/demos/event.vue
index 5cd2dd97..45ac68dc 100644
--- a/src/stepper/demos/event.vue
+++ b/src/stepper/demos/event.vue
@@ -1,38 +1,32 @@
 <template>
-  <div class="cell-base">
-    <t-cell-group title="绑定事件">
-      <t-cell title="标题文字">
+  <div class="stepper-demo">
+    <t-cell title="标题文字（单位）">
+      <template #rightIcon>
         <t-stepper
-          v-model.number="number"
+          v-model="number"
           :step="1"
           :min="0"
           :max="100"
-          :input-width="70"
           @overlimit="onOverlimit"
           @change="onChange"
           @blur="onBlur"
         />
-      </t-cell>
-    </t-cell-group>
+      </template>
+    </t-cell>
   </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, ref } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
 
-export default defineComponent({
-  setup() {
-    const onChange = ($event: number) => {
-      console.log(`change to ${$event}`);
-    };
-    const onBlur = ($event: number) => {
-      console.log(`onBlur to ${$event}`);
-    };
-    return {
-      number: ref(10),
-      onChange,
-      onBlur,
-    };
-  },
-});
+const number = ref(0);
+const onChange = ($event: number) => {
+  console.log(`change to ${$event}`);
+};
+const onBlur = ($event: number) => {
+  console.log(`blur to ${$event}`);
+};
+const onOverlimit = ($type: string) => {
+  console.log(`onOverlimit ${$type}`);
+};
 </script>
diff --git a/src/stepper/demos/mobile.vue b/src/stepper/demos/mobile.vue
index 8a1b79fe..148aa078 100644
--- a/src/stepper/demos/mobile.vue
+++ b/src/stepper/demos/mobile.vue
@@ -3,93 +3,50 @@
     <h1 class="title">Stepper 步进器</h1>
     <p class="summary">用户通过调整“+”按钮、“-”按钮、数字输入框来调整具体需要的数值，可设置最大值和最小值</p>
     <tdesign-demo-block title="01 类型" summary="基础步进器">
-      <div class="stepper-demo">
-        <t-cell title="标题文字">
-          <t-stepper
-            v-model="number1"
-            :step="1"
-            :min="0"
-            :max="100"
-            @overlimit="onOverlimit"
-            @change="onChange"
-            @blur="onBlur"
-          />
-        </t-cell>
-      </div>
+      <baseDemo />
     </tdesign-demo-block>
     <tdesign-demo-block summary="带单位步进器">
-      <div class="stepper-demo">
-        <t-cell title="标题文字（单位）">
-          <template #rightIcon>
-            <t-stepper v-model.number="number2" :step="1" :min="0" :max="100" />
-          </template>
-        </t-cell>
-      </div>
+      <eventDemo />
     </tdesign-demo-block>
     <tdesign-demo-block summary="纯步进器">
       <div style="margin-left: 16px">
-        <t-stepper v-model.number="number3" theme="grey" :step="1" :min="0" :max="100" />
+        <t-stepper v-model="number1" theme="grey" :step="1" :min="0" :max="100" />
       </div>
     </tdesign-demo-block>
     <tdesign-demo-block title="02 类型" summary="步进器状态">
-      <div class="stepper-demo">
-        <t-cell title="禁用">
-          <t-stepper v-model.number="number4" :step="1" :min="0" :max="100" disabled />
-        </t-cell>
-      </div>
-      <div class="stepper-demo">
-        <t-cell title="禁用单位">
-          <t-stepper v-model.number="number5" :step="1" :min="0" :max="100" disabled />
-        </t-cell>
-      </div>
+      <disableDemo />
+      <disableInputDemo />
+
       <div class="stepper-demo">
         <t-cell title="最大值">
-          <t-stepper v-model.number="number6" :step="1" :min="0" :max="999" />
+          <t-stepper v-model="number2" :step="1" :min="0" :max="999" />
         </t-cell>
       </div>
       <div class="stepper-demo">
         <t-cell title="最小值">
-          <t-stepper v-model.number="number7" :step="1" :min="0" :max="999" />
+          <t-stepper v-model="number3" :step="1" :min="0" :max="999" />
         </t-cell>
       </div>
       <div class="stepper-inline-demo">
-        <t-stepper v-model.number="number3" theme="grey" :step="1" :min="0" :max="100" disabled />
-        <t-stepper v-model.number="number6" theme="grey" :step="1" :min="0" :max="100" />
-        <t-stepper v-model.number="number7" theme="grey" :step="1" :min="0" :max="100" />
+        <t-stepper v-model="number4" theme="grey" :step="1" :min="0" :max="100" disabled />
+        <t-stepper v-model="number5" theme="grey" :step="1" :min="0" :max="100" />
+        <t-stepper v-model="number6" theme="grey" :step="1" :min="0" :max="100" />
       </div>
     </tdesign-demo-block>
   </div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import baseDemo from './base.vue';
+import disableDemo from './disabled.vue';
+import disableInputDemo from './disable-input.vue';
+import eventDemo from './event.vue';
 
-export default defineComponent({
-  setup() {
-    const state = reactive({
-      number1: 1,
-      number2: 99,
-      number3: 3,
-      number4: 0,
-      number5: 0,
-      number6: 999,
-      number7: 0,
-    });
-    const onChange = ($event: number) => {
-      console.log(`change to ${$event}`);
-    };
-    const onBlur = ($event: number) => {
-      console.log(`blur to ${$event}`);
-    };
-    const onOverlimit = ($type: string) => {
-      console.log(`onOverlimit ${$type}`);
-    };
-    return {
-      onChange,
-      onBlur,
-      onOverlimit,
-      ...toRefs(state),
-    };
-  },
-});
+const number1 = ref(3);
+const number2 = ref(999);
+const number3 = ref(0);
+const number4 = ref(3);
+const number5 = ref(999);
+const number6 = ref(0);
 </script>
diff --git a/src/stepper/stepper.vue b/src/stepper/stepper.vue
index 4d59cde4..418943f9 100644
--- a/src/stepper/stepper.vue
+++ b/src/stepper/stepper.vue
@@ -8,10 +8,10 @@
       :class="`${name}__input`"
       type="tel"
       :style="inputStyle"
-      pattern="[0-9]*"
       :disabled="disableInput || disabled"
       :readonly="disableInput"
       @blur="handleBlur"
+      @input="handleInput"
     />
     <div :class="[`${name}__plus`, `${disabled || stepperValue >= max ? 't-is-disabled' : ''}`]" @click="plusValue">
       <add-icon :class="`${name}__icon`" />
@@ -20,7 +20,7 @@
 </template>
 
 <script lang="ts">
-import { toRefs, computed, reactive, defineComponent, SetupContext } from 'vue';
+import { toRefs, computed, defineComponent, SetupContext, nextTick } from 'vue';
 import { AddIcon, RemoveIcon } from 'tdesign-icons-vue-next';
 import config from '../config';
 import StepperProps from './props';
@@ -43,41 +43,59 @@ export default defineComponent({
     const [stepperValue] = useDefault<number, TdStepperProps>(props, context.emit, 'value', 'change');
     const emitEvent = useEmitEvent(props, context.emit);
 
-    const { min, max, inputWidth, theme } = toRefs(props);
-    const isPureMode = theme.value === 'grey';
-    const inputStyle = inputWidth ? { width: `${inputWidth.value}px` } : '';
+    const { min, max, step, inputWidth, theme, disabled } = toRefs(props);
+    const isPureMode = computed(() => theme.value === 'grey');
+    const inputStyle = computed(() => (inputWidth ? { width: `${inputWidth.value}px` } : ''));
+
+    const isDisabled = (type: 'minus' | 'plus') => {
+      if (disabled.value) return true;
+      if (type === 'minus' && stepperValue.value <= min.value) {
+        return true;
+      }
+      if (type === 'plus' && stepperValue.value >= max.value) {
+        return true;
+      }
+      return false;
+    };
+
+    const formatValue = (value: number) =>
+      Math.max(Math.min(max.value, value, Number.MAX_SAFE_INTEGER), min.value, Number.MIN_SAFE_INTEGER);
+
+    const updateValue = (value: number) => {
+      stepperValue.value = formatValue(value);
+    };
 
-    const format = (val: number) =>
-      Math.min(Math.max(min.value, val, Number.MIN_SAFE_INTEGER), max.value, Number.MAX_SAFE_INTEGER);
     const plusValue = () => {
-      if (stepperValue.value + props.step > props.max || props.disabled) return;
-      stepperValue.value += props.step;
+      if (isDisabled('plus')) {
+        emitEvent('overlimit', 'plus');
+        return;
+      }
+      updateValue(Number(stepperValue.value) + step.value);
     };
+
     const minusValue = () => {
-      if (stepperValue.value - props.step < props.min || props.disabled) return;
-      stepperValue.value -= props.step;
-    };
-    const changeValue = (e: Event) => {
-      const value = (e.target as HTMLTextAreaElement).value.split('.')[0].replace(/[^-0-9]/g, '');
-      if (value.trim() === '') {
-        stepperValue.value = 0;
-      } else {
-        handleOverlimit(Number(value));
-        stepperValue.value = format(Number(value));
+      if (isDisabled('minus')) {
+        emitEvent('overlimit', 'minus');
+        return;
       }
+      updateValue(Number(stepperValue.value) - step.value);
+    };
+
+    const handleInput = (e: Event) => {
+      handleChange(e);
     };
+
+    const handleChange = (e: Event) => {
+      const value = (e.target as HTMLTextAreaElement).value.match(/^\d+\.\d+|^\d+/g);
+      if (isNaN(Number(value))) return;
+      const formattedValue = formatValue(Number(value));
+      updateValue(Number(formattedValue));
+    };
+
     const handleBlur = (e: FocusEvent) => {
-      changeValue(e);
+      handleChange(e);
       emitEvent('blur', stepperValue.value);
     };
-    const handleOverlimit = (value: number) => {
-      if (value < Math.max(min.value, Number.MIN_SAFE_INTEGER)) {
-        emitEvent('overlimit', 'minus');
-      }
-      if (value > Math.min(max.value, Number.MAX_SAFE_INTEGER)) {
-        emitEvent('overlimit', 'plus');
-      }
-    };
 
     return {
       name,
@@ -85,7 +103,8 @@ export default defineComponent({
       minusValue,
       stepperValue,
       plusValue,
-      changeValue,
+      handleInput,
+      handleChange,
       inputStyle,
       handleBlur,
       isPureMode,
diff --git a/src/swiper/swiper.vue b/src/swiper/swiper.vue
index b79ed4ff..9024226b 100644
--- a/src/swiper/swiper.vue
+++ b/src/swiper/swiper.vue
@@ -59,7 +59,7 @@ import {
   ComponentPublicInstance,
 } from 'vue';
 import { ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-vue-next';
-import { SwipeDirection, useSwipe } from '@vueuse/core';
+import { useSwipe } from '@vueuse/core';
 import SwiperProps from './props';
 import config from '../config';
 import { renderTNode, useDefault, TNode } from '../shared';
@@ -109,16 +109,16 @@ export default defineComponent({
       return activeIndex + 1;
     });
     const childCount = computed(() => state.children.length);
-    const getContainer = (): HTMLDivElement => self?.proxy?.$el.querySelector('.t-swiper__container');
+    const getContainer = (): HTMLDivElement => self?.proxy?.$el.querySelector(`.${name}__container`);
     // const getContainer = (): HTMLDivElement => swiperContainer.value as any;
     const initSwiper = () => {
       const _swiperContainer = getContainer();
       _swiperContainer.querySelectorAll('.copy-item').forEach((ele) => {
         _swiperContainer.removeChild(ele);
       });
-      const items = _swiperContainer.querySelectorAll('.t-swiper-item');
+      const items = _swiperContainer.querySelectorAll(`.${name}-item`);
       state.itemLength = _swiperContainer.children?.length || 0;
-      const itemWidth = _swiperContainer.querySelector('.t-swiper-item')?.getBoundingClientRect().width || 0;
+      const itemWidth = _swiperContainer.querySelector(`.${name}-item`)?.getBoundingClientRect().width || 0;
       state.itemWidth = itemWidth;
       if (items.length <= 0) return false;
       if (
diff --git a/src/tab-bar/tab-bar-item.vue b/src/tab-bar/tab-bar-item.vue
index 837bdc04..d95ab673 100644
--- a/src/tab-bar/tab-bar-item.vue
+++ b/src/tab-bar/tab-bar-item.vue
@@ -91,6 +91,7 @@ export default defineComponent({
         isSpread.value = !isSpread.value;
         if (!isToggleCurrent.value) {
           updateChild([currentName]);
+          return;
         }
       }
       updateChild(currentName);
diff --git a/src/tab-bar/tab-bar.vue b/src/tab-bar/tab-bar.vue
index e26b570c..01d900c3 100644
--- a/src/tab-bar/tab-bar.vue
+++ b/src/tab-bar/tab-bar.vue
@@ -5,7 +5,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, ref, provide, watch, Ref, computed, SetupContext } from 'vue';
+import { defineComponent, ref, provide, Ref, computed, SetupContext } from 'vue';
 import TabBarProps from './props';
 import config from '../config';
 import { useDefault } from '../shared';
diff --git a/src/toast/toast.vue b/src/toast/toast.vue
index d2b69949..e750c7fd 100644
--- a/src/toast/toast.vue
+++ b/src/toast/toast.vue
@@ -53,15 +53,17 @@ export default defineComponent({
       },
     ]);
 
-    const toastOverlayProps = {
-      preventScrollThrough: props.preventScrollThrough,
-      visible: props.showOverlay,
-    };
+    const customOverlayProps = computed(() => {
+      const toastOverlayProps = {
+        preventScrollThrough: props.preventScrollThrough,
+        visible: props.showOverlay,
+      };
 
-    const customOverlayProps = computed(() => ({
-      ...props.overlayProps,
-      ...toastOverlayProps,
-    }));
+      return {
+        ...props.overlayProps,
+        ...toastOverlayProps,
+      };
+    });
 
     return {
       name: ref(name),
diff --git a/src/upload/type.ts b/src/upload/type.ts
index a8f17793..f57243a5 100644
--- a/src/upload/type.ts
+++ b/src/upload/type.ts
@@ -157,6 +157,10 @@ export interface TdUploadProps {
 }
 
 export interface UploadFile {
+  /**
+   * 源文件
+   */
+  fileRaw: File;
   /**
    * 上一次变更的时间
    */
diff --git a/src/upload/upload.vue b/src/upload/upload.vue
index d86d46ba..1210d666 100644
--- a/src/upload/upload.vue
+++ b/src/upload/upload.vue
@@ -150,22 +150,27 @@ export default defineComponent({
       });
     };
 
-    const handleReload = (file: UploadFile) => {
+    const handleReload = (file: any) => {
       uploadFiles([file]);
     };
 
-    const handleChange = (event: Event) => {
-      const { files } = <HTMLInputElement>event.target;
-      if (props.disabled || !files) return;
-      const input = <HTMLInputElement>inputRef.value;
-      const formatFiles = formatFileToUploadFile(files);
+    const handleChange = () => {
+      const input = inputRef.value;
+      if (props.disabled || !input || !input.files) return;
+      const formatFiles = formatFileToUploadFile(input.files);
       emitEvent('select-change', [...formatFiles]);
       uploadFiles(formatFiles);
       input.value = '';
     };
 
-    const formatFileToUploadFile = (files: any): UploadFile[] => {
-      if (!props.format || !isFunction(props.format)) return files;
+    const formatFileToUploadFile = (files: FileList): File[] => {
+      if (!props.format || !isFunction(props.format)) {
+        const res = [];
+        for (let i = 0; i < files.length; i++) {
+          res.push(files[i]);
+        }
+        return res;
+      }
 
       const NewFiles = [...files];
       NewFiles.forEach((item) => {
@@ -206,7 +211,7 @@ export default defineComponent({
       return isOverSize;
     };
 
-    const uploadFiles = (files: UploadFile[]) => {
+    const uploadFiles = (files: File[]) => {
       const { max } = toRefs(props);
       let tmpFiles = [...files];
       if (max.value) {
@@ -215,9 +220,10 @@ export default defineComponent({
           console.warn(`TDesign Upload Warn: you can only upload ${max.value} files`);
         }
       }
-      tmpFiles.forEach((fileRaw: UploadFile) => {
+      tmpFiles.forEach((fileRaw: any) => {
         const uploadFile: UploadFile = {
           ...fileRaw,
+          fileRaw,
           lastModified: fileRaw.lastModified,
           name: fileRaw.name,
           size: fileRaw.size,
@@ -301,9 +307,11 @@ export default defineComponent({
         const request = xhr;
         xhrReq.value = request({
           action: props.action,
-          data: props.data || {},
+          data: {
+            file: file.fileRaw,
+            ...props.data,
+          },
           method: props.method,
-          file,
           headers: props.headers || {},
           withCredentials: props.withCredentials,
           onError: handleError,
diff --git a/tsconfig.json b/tsconfig.json
index 34440850..0d1c4e74 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,6 +24,7 @@
       "@/*": ["./src/*"],
       "tdesign-moile-vue": ["./src/*"]
     },
+    "types": ["vitest/globals"],
     "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
   },
   "rules": { "trailing-comma": false },


Test patch:
diff --git a/site/web/test-coverage.js b/site/web/test-coverage.js
index e15ef27b..ef34f280 100644
--- a/site/web/test-coverage.js
+++ b/site/web/test-coverage.js
@@ -1,1 +1,11 @@
-module.exports = {"button":{"statements":"98.57%","branches":"58.82%","functions":"100%","lines":"98.57%"},"checkbox":{"statements":"93.47%","branches":"75.8%","functions":"80%","lines":"93.47%"},"fab":{"statements":"100%","branches":"100%","functions":"100%","lines":"100%"},"shared":{"statements":"70.46%","branches":"61.11%","functions":"25%","lines":"70.46%"}}
+module.exports = {
+  button: { statements: '98.57%', branches: '58.82%', functions: '100%', lines: '98.57%' },
+  checkbox: { statements: '93.47%', branches: '75.8%', functions: '80%', lines: '93.47%' },
+  drawer: { statements: '100%', branches: '93.75%', functions: '100%', lines: '100%' },
+  fab: { statements: '100%', branches: '100%', functions: '100%', lines: '100%' },
+  overlay: { statements: '92.55%', branches: '100%', functions: '20%', lines: '92.55%' },
+  popup: { statements: '95%', branches: '64.7%', functions: '55.55%', lines: '95%' },
+  shared: { statements: '70.58%', branches: '63.15%', functions: '25%', lines: '70.58%' },
+  switch: { statements: '98.05%', branches: '92.85%', functions: '100%', lines: '98.05%' },
+  toast: { statements: '100%', branches: '80%', functions: '100%', lines: '100%' },
+};
diff --git a/src/action-sheet/__test__/index.test.jsx b/src/action-sheet/__test__/index.test.jsx
new file mode 100644
index 00000000..a3b2e92f
--- /dev/null
+++ b/src/action-sheet/__test__/index.test.jsx
@@ -0,0 +1,176 @@
+import { describe, it, expect, vi } from 'vitest';
+import { DOMWrapper, mount } from '@vue/test-utils';
+import ActionSheet from '../action-sheet.vue'
+import MenuGrid from '../menu-grid.vue'
+import { ref } from 'vue';
+
+describe('ActionSheet', () => {
+  describe('props', () => {
+    it ('showCancel', async () => {
+      const wrapper = mount(ActionSheet, {
+        props: {
+          items: [],
+          visible: true
+        }
+      })
+      expect(wrapper.find('.t-action-sheet__action').exists()).toEqual(true)
+      await wrapper.setProps({
+        showCancel: false
+      })
+      expect(wrapper.find('.t-action-sheet__action').exists()).toEqual(false)
+    })
+
+    it ('cancelText', async () => {
+      const wrapper = mount(ActionSheet, {
+        props: {
+          items: [],
+          visible: true
+        }
+      })
+      expect(wrapper.find('.t-action-sheet__action').text()).toEqual('取消')
+      await wrapper.setProps({
+        cancelText: 'Cancel'
+      })
+      expect(wrapper.find('.t-action-sheet__action').text()).toEqual('Cancel')
+    })
+
+    it ('type', async () => {
+      const wrapper = mount(ActionSheet, {
+        props: {
+          type: 'list',
+          items: [],
+          visible: true
+        }
+      })
+      expect(wrapper.find('.t-action-sheet__panel-list').exists()).toEqual(true)
+      expect(wrapper.find('.t-action-sheet__panel-grid').exists()).toEqual(false)
+      await wrapper.setProps({
+        type: 'grid'
+      })
+      expect(wrapper.find('.t-action-sheet__panel-list').exists()).toEqual(false)
+      expect(wrapper.find('.t-action-sheet__panel-grid').exists()).toEqual(true)
+    })
+
+    it ('count', async () => {
+      const wrapper = mount(ActionSheet, {
+        props: {
+          type: 'grid',
+          items: ['1', '2', '3'],
+          visible: true,
+          count: 2
+        }
+      })
+      expect(wrapper.findComponent(MenuGrid).props().count).toEqual(2)
+      await wrapper.setProps({
+        count: 3
+      })
+      expect(wrapper.findComponent(MenuGrid).props().count).toEqual(3)
+    })
+
+    it ('visible', async () => {
+      const wrapper = mount(ActionSheet, {
+        props: {
+          visible: true,
+          items: []
+        }
+      })
+      expect(wrapper.find('.t-popup--content').isVisible()).toEqual(true)
+      await wrapper.setProps({
+        visible: false
+      })
+      expect(wrapper.find('.t-popup--content').isVisible()).toEqual(false)
+    })
+  })
+
+  describe('events', () => {
+    it ('onSelected', () => {
+      const onSelected = vi.fn()
+      const wrapper = mount(() => <ActionSheet visible items={['确定', '删除']} onSelected={onSelected} />)
+      wrapper.find('button').trigger('click')
+      expect(onSelected).toHaveBeenCalledTimes(1)
+      expect(onSelected).toHaveBeenLastCalledWith('确定', 0)
+      wrapper.findAll('button')[1].trigger('click')
+      expect(onSelected).toHaveBeenCalledTimes(2)
+      expect(onSelected).toHaveBeenLastCalledWith('删除', 1)
+    })
+
+    it('onCancel', () => {
+      const onCancel = vi.fn()
+      const wrapper = mount(() => <ActionSheet visible items={['确定', '删除']} onCancel={onCancel} />)
+      wrapper.find('.t-action-sheet__action').trigger('click')
+      expect(onCancel).toHaveBeenCalledTimes(1)
+    })
+
+    it('onClose', () => {
+      const onClose = vi.fn()
+      const wrapper = mount(() => <ActionSheet visible items={['确定', '删除']} onClose={onClose} />)
+      wrapper.find('.t-overlay').trigger('click')
+      expect(onClose).toHaveBeenCalledTimes(1)
+    })
+  })
+})
+
+describe('MenuGrid', () => {
+  function triggerTouchEvent (event, ele, clientX) {
+    ele.trigger(event, {
+      touches: [
+        {
+          clientX
+        }
+      ],
+      changedTouches: [
+        {
+          clientX
+        }
+      ]
+    })
+  }
+  async function simulateSwipe(ele, dir) {
+    await triggerTouchEvent('touchstart', ele, 0)
+    await triggerTouchEvent('touchmove', ele, 0)
+    const distance = 51
+    await triggerTouchEvent('touchend', ele, dir === 'prev' ? distance : -distance)
+  }
+
+  it ('count', async () => {
+    const wrapper = mount(MenuGrid, {
+      props: {
+        items: [{
+          label: '1'
+        }, {
+          label: '2'
+        }, {
+          label: '3'
+        }],
+        count: 1
+      }
+    })
+    expect(wrapper.findAll('.t-action-sheet__menu')).toHaveLength(3)
+    await wrapper.setProps({
+      count: 2
+    })
+    expect(wrapper.findAll('.t-action-sheet__menu')).toHaveLength(2)
+  })
+
+  it ('touch move', async () => {
+    const wrapper = mount(MenuGrid, {
+      props: {
+        items: [{
+          label: '1'
+        }, {
+          label: '2'
+        }, {
+          label: '3'
+        }],
+        count: 1
+      }
+    })
+
+    Object.defineProperty(wrapper.element, 'offsetWidth', { configurable: true, value: 500 })
+    const slider = wrapper.find('.t-action-sheet__menu-slider')
+    await simulateSwipe(slider, 'next')
+    expect(getComputedStyle(slider.element).transform).toEqual('translate3d(-500px, 0, 0)')
+    await simulateSwipe(slider, 'prev')
+    expect(getComputedStyle(slider.element).transform).toEqual('translate3d(0px, 0, 0)')
+  })
+})
diff --git a/src/back-top/__test__/index.test.jsx b/src/back-top/__test__/index.test.jsx
new file mode 100644
index 00000000..56278bc6
--- /dev/null
+++ b/src/back-top/__test__/index.test.jsx
@@ -0,0 +1,81 @@
+import { describe, it, expect, vi } from 'vitest';
+import BackTop from '../back-top.vue'
+import { mount } from '@vue/test-utils';
+import { AppIcon as TIconApp } from 'tdesign-icons-vue-next';
+
+describe('BackTop', () => {
+  describe('props', () => {
+    it (': fixed', async () => {
+      const wrapper = mount(BackTop)
+      expect(wrapper.find('.t-is-fixed').exists()).toEqual(true)
+      await wrapper.setProps({
+        fixed: false
+      })
+      expect(wrapper.find('.t-is-fixed').exists()).toEqual(false)
+    })
+
+    it(': onToTop', () => {
+      const onToTop = vi.fn()
+      const wrapper = mount(BackTop, {
+        props: { onToTop }
+      })
+      wrapper.find('.t-back-top').trigger('click')
+      expect(onToTop).toHaveBeenCalledTimes(1)
+    })
+
+    it(': text', async () => {
+      const wrapper = mount(BackTop)
+      expect(wrapper.find('.t-back-top__text').exists()).toEqual(false)
+      await wrapper.setProps({
+        text: '返回'
+      })
+      expect(wrapper.find('.t-back-top__text').text()).toEqual('返回')
+    })
+
+    it(': theme', async () => {
+      const wrapper = mount(BackTop)
+      expect(wrapper.find('.t-back-top').classes()).toContain('t-back-top--round')
+      await wrapper.setProps({
+        theme: 'half-round'
+      })
+      expect(wrapper.find('.t-back-top').classes()).toContain('t-back-top--half-round')
+    })
+
+    it(': target', async () => {
+      const wrapper = mount(BackTop, {
+        props: {
+          target: () => ({
+            getBoundingClientRect: () => {
+              return {
+                top: 10
+              }
+            }
+          })
+        }
+      })
+      await wrapper.find('.t-back-top').trigger('click')
+      expect(window.document.documentElement.scrollTop).toEqual(10)
+    })
+  })
+
+  describe('slots', () => {
+    it(': icon', () => {
+      const icon = () => <TIconApp />
+
+      const wrapper = mount(BackTop, {
+        slots: {
+          icon
+        }
+      })
+      expect(wrapper.find('.t-back-top > svg').classes()).toContain('t-icon')
+    })
+  })
+
+  describe('events', () => {
+    it(': to-top', async () => {
+      const wrapper = mount(BackTop)
+      wrapper.find('.t-back-top').trigger('click')
+      expect(wrapper.emitted('to-top')).toHaveLength(1)
+    });
+  })
+})
diff --git a/src/button/demos/test.vue b/src/button/demos/test.vue
index 054c08b7..fb4546af 100644
--- a/src/button/demos/test.vue
+++ b/src/button/demos/test.vue
@@ -3,10 +3,10 @@
     <p>正确</p>
     <div class="tdesign-demo-block">
       <t-button theme="primary">主要按钮</t-button>
-      <t-button variant="outline" >次要按钮</t-button>
+      <t-button variant="outline">次要按钮</t-button>
     </div>
 
-    <br/>
+    <br />
     <p>错误</p>
     <div class="tdesign-demo-block">
       <t-button theme="primary">主要按钮</t-button>
@@ -17,5 +17,6 @@
 
 <script lang="ts">
 import { defineComponent } from 'vue';
+
 export default defineComponent({});
 </script>
diff --git a/src/checkbox/__test__/index.test.tsx b/src/checkbox/__test__/index.test.tsx
index c3b30e43..0a4fe186 100644
--- a/src/checkbox/__test__/index.test.tsx
+++ b/src/checkbox/__test__/index.test.tsx
@@ -13,21 +13,21 @@ describe('Checkbox', () => {
               <Checkbox value="1" ref="1" />
               <Checkbox value="2" ref="2" />
             </CheckboxGroup>
-          )
-        }
+          );
+        },
       });
 
       expect(checked.value.length).toBe(0);
 
-      const target = wrapper.findComponent({ ref: '1'});     
-      await target.find('.t-checkbox__original-left').trigger('click')
-      
-      expect(wrapper.findComponent({ ref: '1'}).vm.isChecked).toBe(true);
+      const target = wrapper.findComponent({ ref: '1' });
+      await target.find('.t-checkbox__original-left').trigger('click');
+
+      expect(wrapper.findComponent({ ref: '1' }).vm.isChecked).toBe(true);
       expect(checked.value).toContain('1');
 
-      await target.find('.t-checkbox__original-left').trigger('click')
+      await target.find('.t-checkbox__original-left').trigger('click');
 
-      expect(wrapper.findComponent({ ref: '1'}).vm.isChecked).toBe(false);
+      expect(wrapper.findComponent({ ref: '1' }).vm.isChecked).toBe(false);
       expect(checked.value.length).toBe(0);
     });
 
@@ -40,20 +40,20 @@ describe('Checkbox', () => {
               <Checkbox value="2" ref="2" />
               <Checkbox value="3" ref="3" />
             </CheckboxGroup>
-          )
-        }
+          );
+        },
       });
 
-      await wrapper.findComponent({ ref: '1'}).find('.t-checkbox__original-left').trigger('click')
-      
-      expect(wrapper.findComponent({ ref: '2'}).vm.isChecked).toBe(true);
-      expect(wrapper.findComponent({ ref: '3'}).vm.isChecked).toBe(true);
+      await wrapper.findComponent({ ref: '1' }).find('.t-checkbox__original-left').trigger('click');
+
+      expect(wrapper.findComponent({ ref: '2' }).vm.isChecked).toBe(true);
+      expect(wrapper.findComponent({ ref: '3' }).vm.isChecked).toBe(true);
 
-      await wrapper.findComponent({ ref: '1'}).find('.t-checkbox__original-left').trigger('click')
+      await wrapper.findComponent({ ref: '1' }).find('.t-checkbox__original-left').trigger('click');
 
-      expect(wrapper.findComponent({ ref: '2'}).vm.isChecked).toBe(false);
-      expect(wrapper.findComponent({ ref: '3'}).vm.isChecked).toBe(false);
-    })
+      expect(wrapper.findComponent({ ref: '2' }).vm.isChecked).toBe(false);
+      expect(wrapper.findComponent({ ref: '3' }).vm.isChecked).toBe(false);
+    });
 
     test('max', async () => {
       const checked = ref([]);
@@ -65,18 +65,18 @@ describe('Checkbox', () => {
               <Checkbox value="2" ref="2" />
               <Checkbox value="3" ref="3" />
             </CheckboxGroup>
-          )
-        }
+          );
+        },
       });
 
-      await wrapper.findComponent({ ref: '1'}).find('.t-checkbox__original-left').trigger('click')
-      
-      expect(wrapper.findComponent({ ref: '1'}).vm.isChecked).toBe(true);
+      await wrapper.findComponent({ ref: '1' }).find('.t-checkbox__original-left').trigger('click');
 
-      await wrapper.findComponent({ ref: '2'}).find('.t-checkbox__original-left').trigger('click')
+      expect(wrapper.findComponent({ ref: '1' }).vm.isChecked).toBe(true);
 
-      expect(wrapper.findComponent({ ref: '2'}).vm.isChecked).toBe(true);
-      expect(wrapper.findComponent({ ref: '3'}).vm.isDisabled).toBe(true);
-    })
-  })
-})
+      await wrapper.findComponent({ ref: '2' }).find('.t-checkbox__original-left').trigger('click');
+
+      expect(wrapper.findComponent({ ref: '2' }).vm.isChecked).toBe(true);
+      expect(wrapper.findComponent({ ref: '3' }).vm.isDisabled).toBe(true);
+    });
+  });
+});
diff --git a/src/collapse/__test__/index.test.jsx b/src/collapse/__test__/index.test.jsx
new file mode 100644
index 00000000..bb792e58
--- /dev/null
+++ b/src/collapse/__test__/index.test.jsx
@@ -0,0 +1,225 @@
+import { ref, reactive } from 'vue';
+import { mount } from '@vue/test-utils';
+import { Collapse, CollapsePanel } from '../index';
+
+describe('Collapse', () => {
+  describe('collapse props', () => {
+    test('defaultExpandAll', async () => {
+      const defaultExpandAll = ref(false);
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse defaultExpandAll={defaultExpandAll}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+
+      expect(panel1.classes()).not.toContain('t-collapse-panel--active');
+      expect(panel2.classes()).not.toContain('t-collapse-panel--active');
+
+      defaultExpandAll.value = true;
+      await wrapper.vm.$nextTick();
+
+      expect(panel1.classes()).toContain('t-collapse-panel--active');
+      expect(panel2.classes()).toContain('t-collapse-panel--active');
+    });
+
+    test('default-value', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse default-value={['1']}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+
+      expect(panel1.classes()).toContain('t-collapse-panel--active');
+      expect(panel2.classes()).not.toContain('t-collapse-panel--active');
+    });
+
+    test('value', async () => {
+      const val = ref([]);
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse v-model={val.value}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+      await panel1.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(1);
+      expect(val.value).toContain('1');
+
+      await panel2.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(2);
+      expect(val.value).toContain('2');
+    });
+
+    test('expandMutex', async () => {
+      const val = ref([]);
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse expandMutex v-model={val.value}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+      await panel1.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(1);
+      expect(val.value).toContain('1');
+
+      await panel2.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(1);
+      expect(val.value).toContain('2');
+    });
+
+    test('onChange', async () => {
+      const handleChange = vi.fn();
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse onChange={handleChange}>
+              <CollapsePanel ref="1" value="1" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+      await panel.find('.t-collapse-panel__header').trigger('click')
+
+      expect(handleChange).toHaveBeenCalled();
+    });
+
+    test('disabled', async () => {
+      const disabled = ref(false);
+      const handleChange = vi.fn();
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse disabled={disabled.value} onChange={handleChange}>
+              <CollapsePanel ref="1" value="1" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+      await panel.find('.t-collapse-panel__header').trigger('click');
+
+      expect(handleChange).toHaveBeenCalled();
+
+      disabled.value = true;
+      await wrapper.vm.$nextTick();
+      await panel.find('.t-collapse-panel__header').trigger('click');
+
+      expect(handleChange).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('collapse-panel props', () => {
+    test('header and content', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse>
+              <CollapsePanel ref="1" value="1" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      expect(panel.find('.t-collapse-panel__title').text()).toBe('标题');
+      expect(panel.find('.t-collapse-panel__body').text()).toBe('内容');
+    });
+
+    test('disabled', async () => {
+      const handleChange = vi.fn();
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse onChange={handleChange}>
+              <CollapsePanel ref="1" value="1" disabled header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      await panel.find('.t-collapse-panel__header').trigger('click')
+
+      expect(handleChange).not.toHaveBeenCalled();
+    });
+
+    test('headerRightContent', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse>
+              <CollapsePanel ref="1" value="1" headerRightContent="操作" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      expect(panel.find('.t-collapse-panel__header-right').text()).toBe('操作');
+    });
+
+    test('slot', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse>
+              <CollapsePanel ref="1" value="1">
+                {{
+                  header: () => <h4>标题</h4>,
+                  headerRightContent: () => <span>操作</span>,
+                  default: () => <div>内容</div>,
+                }}
+              </CollapsePanel>
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      expect(panel.find('.t-collapse-panel__title > h4').html()).toBe('<h4>标题</h4>');
+      expect(panel.find('.t-collapse-panel__header-extra > span').html()).toBe('<span>操作</span>');
+      expect(panel.find('.t-collapse-panel__body > div').html()).toBe('<div>内容</div>');
+    });
+  });
+});
diff --git a/src/count-down/__test__/index.test.jsx b/src/count-down/__test__/index.test.jsx
new file mode 100644
index 00000000..4be3bebc
--- /dev/null
+++ b/src/count-down/__test__/index.test.jsx
@@ -0,0 +1,103 @@
+import { nextTick } from 'vue';
+import { describe, it, expect, vi } from 'vitest';
+import { mount } from '@vue/test-utils';
+import CountDown from '../count-down.vue';
+
+const sleep = (duration) =>
+  new Promise((resolve) =>
+    setTimeout(() => {
+      resolve();
+    }, duration),
+  );
+
+describe('countdown.vue', async () => {
+  describe('props', async () => {
+    it('time', async () => {
+      const wrapper = mount(<CountDown time={2000}></CountDown>);
+      expect(wrapper.text()).toContain('00:00:02');
+      await sleep(1000);
+      expect(wrapper.text()).toContain('00:00:01');
+      await sleep(500);
+      expect(wrapper.text()).toContain('00:00:00');
+      await sleep(500);
+      await nextTick();
+      expect(wrapper.text()).toContain('default');
+    });
+
+    it('content', async () => {
+      const wrapper = mount(<CountDown time={1000} content="ok"></CountDown>);
+      await sleep(1500);
+      await nextTick();
+      expect(wrapper.text()).toContain('ok');
+
+      await wrapper.setProps({ content: 'change' });
+      expect(wrapper.text()).toContain('change');
+    });
+
+    it('format', async () => {
+      const wrapper = mount(<CountDown time={100000} format="mm:ss"></CountDown>);
+      expect(wrapper.text()).not.toContain('00:01:40');
+      expect(wrapper.text()).toContain('01:40');
+
+      const wrapper2 = mount(<CountDown time={100000} format="ss"></CountDown>);
+      expect(wrapper2.text()).not.toContain(':40');
+      expect(wrapper2.text()).toContain('40');
+
+      const wrapper3 = mount(<CountDown time={100000} format="中文"></CountDown>);
+      expect(wrapper3.find('.t-countdown---split-with-unit').exists()).toBe(false);
+
+      const wrapper4 = mount(<CountDown></CountDown>);
+      expect(wrapper4).toBeTruthy();
+    });
+
+    it('millisecond', async () => {
+      const wrapper = mount(<CountDown time={100000} millisecond={true}></CountDown>);
+      expect(wrapper.text()).toContain('00:01:40:000');
+    });
+
+    it('autoStart', async () => {
+      const wrapper = mount(<CountDown time={100000} autoStart={false}></CountDown>);
+      await sleep(1000);
+      expect(wrapper.text()).toContain('00:01:40');
+    });
+
+    it('size', async () => {
+      const wrapper = mount(<CountDown time={100000}></CountDown>);
+      expect(wrapper.find('.t-countdown--small').exists()).toBe(true);
+    });
+
+    it('theme', async () => {
+      const wrapper = mount(<CountDown time={100000}></CountDown>);
+      expect(wrapper.find('.t-countdown--default').exists()).toBe(true);
+    });
+  });
+
+  describe('events', async () => {
+    it('change', async () => {
+      const onChange = vi.fn();
+      const wrapper = mount(<CountDown time={2000} onChange={onChange}></CountDown>);
+      await sleep(1000);
+      expect(onChange).toHaveBeenCalled();
+    });
+
+    it('finish', async () => {
+      const onFinish = vi.fn();
+      const wrapper = mount(<CountDown time={1000} onFinish={onFinish}></CountDown>);
+      await sleep(1500);
+      expect(onFinish).toHaveBeenCalled();
+    });
+  });
+
+  describe('slots', async () => {
+    it('content', async () => {
+      const temp = <div>测试</div>;
+      const wrapper = mount(<CountDown time={1000}></CountDown>, {
+        slots: {
+          content: temp,
+        },
+      });
+      await sleep(1500);
+      expect(wrapper.text()).toContain('测试');
+    });
+  });
+});
diff --git a/src/date-time-picker/__test__/__snapshots__/demo.test.jsx.snap b/src/date-time-picker/__test__/__snapshots__/demo.test.jsx.snap
index d78610bb..62a66905 100644
--- a/src/date-time-picker/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/date-time-picker/__test__/__snapshots__/demo.test.jsx.snap
@@ -69,7 +69,7 @@ exports[`DateTimePicker > DateTimePicker baseVue demo works fine 1`] = `
     </transition-stub>
     <transition-stub>
       <div
-        class="t-popup--content t-popup--content-top"
+        class="t-popup--content t-popup--content-bottom"
         style="display: none;"
       >
         
@@ -1318,7 +1318,7 @@ exports[`DateTimePicker > DateTimePicker hourMinuteVue demo works fine 1`] = `
     </transition-stub>
     <transition-stub>
       <div
-        class="t-popup--content t-popup--content-top"
+        class="t-popup--content t-popup--content-bottom"
         style="display: none;"
       >
         
@@ -6960,7 +6960,7 @@ exports[`DateTimePicker > DateTimePicker renderLabelDateVue demo works fine 1`]
     </transition-stub>
     <transition-stub>
       <div
-        class="t-popup--content t-popup--content-top"
+        class="t-popup--content t-popup--content-bottom"
         style="display: none;"
       >
         
@@ -7456,7 +7456,7 @@ exports[`DateTimePicker > DateTimePicker startEndDateVue demo works fine 1`] = `
     </transition-stub>
     <transition-stub>
       <div
-        class="t-popup--content t-popup--content-top"
+        class="t-popup--content t-popup--content-bottom"
         style="display: none;"
       >
         
diff --git a/src/dialog/__test__/__snapshots__/demo.test.jsx.snap b/src/dialog/__test__/__snapshots__/demo.test.jsx.snap
index 6dd64822..9a17085c 100644
--- a/src/dialog/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/dialog/__test__/__snapshots__/demo.test.jsx.snap
@@ -274,7 +274,7 @@ exports[`Dialog > Dialog feedbackVue demo works fine 1`] = `
                     id="root"
                   >
                     <div
-                      class="t-dialog__header"
+                      class="t-dialog__header t-dialog__header--has-content"
                     >
                       
                       <div
@@ -363,7 +363,7 @@ exports[`Dialog > Dialog feedbackVue demo works fine 1`] = `
                     id="root"
                   >
                     <div
-                      class="t-dialog__header"
+                      class="t-dialog__header t-dialog__header--has-content"
                     >
                       
                       <div
@@ -545,7 +545,7 @@ exports[`Dialog > Dialog feedbackVue demo works fine 1`] = `
                     id="root"
                   >
                     <div
-                      class="t-dialog__header"
+                      class="t-dialog__header t-dialog__header--has-content"
                     >
                       
                       <div
@@ -667,7 +667,7 @@ exports[`Dialog > Dialog inputVue demo works fine 1`] = `
           id="root"
         >
           <div
-            class="t-dialog__header"
+            class="t-dialog__header t-dialog__header--has-content"
           >
             
             <div
@@ -887,7 +887,7 @@ exports[`Dialog > Dialog mobileVue demo works fine 1`] = `
               id="root"
             >
               <div
-                class="t-dialog__header"
+                class="t-dialog__header t-dialog__header--has-content"
               >
                 
                 <div
@@ -954,7 +954,7 @@ exports[`Dialog > Dialog mobileVue demo works fine 1`] = `
               id="root"
             >
               <div
-                class="t-dialog__header"
+                class="t-dialog__header t-dialog__header--has-content"
               >
                 
                 <div
@@ -1440,7 +1440,7 @@ exports[`Dialog > Dialog mobileVue demo works fine 1`] = `
               id="root"
             >
               <div
-                class="t-dialog__header"
+                class="t-dialog__header t-dialog__header--has-content"
               >
                 
                 <div
diff --git a/src/dialog/__test__/index.test.jsx b/src/dialog/__test__/index.test.jsx
new file mode 100644
index 00000000..5e83cecb
--- /dev/null
+++ b/src/dialog/__test__/index.test.jsx
@@ -0,0 +1,183 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import { nextTick } from 'vue';
+import Dialog from '../dialog.vue';
+import Button from '../../button/button.vue';
+
+describe('dialog', () => {
+  describe('props', () => {
+    it(': visible	', async () => {
+      const visible = true;
+      const title = 'this is a title';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+        },
+      });
+
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(true)
+      expect(wrapper.find('.t-dialog__header').text()).toBe(title);
+      await wrapper.setProps({
+        visible: false,
+      })
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(false)
+
+    })
+
+    it(': title', async () => {
+      const visible = true;
+      const title = 'old title';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+        },
+      });
+      expect(wrapper.find('.t-dialog__header').text()).toBe(title);
+      const newTitle = 'new title'
+      await wrapper.setProps({
+        title: newTitle
+      })
+      expect(wrapper.find('.t-dialog__header').text()).toBe(newTitle);
+    })
+
+    it(': content', async () => {
+      const visible = true;
+      const title = 'this is title';
+      const content = 'content is string';
+
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+          content,
+        }
+      });
+
+      expect(wrapper.find('.t-dialog__body').text()).toBe(content);
+      const newContent = 'new content';
+      await wrapper.setProps({
+        content: newContent
+      })
+      expect(wrapper.find('.t-dialog__body').text()).toBe(newContent);
+    });
+
+    it(': buttonLayout', async () => {
+      const visible = true;
+      const selector = 't-dialog__vertical-footer';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+        },
+      });
+      expect(wrapper.find('.t-dialog__footer').classes().includes(selector)).toBeFalsy();
+      await wrapper.setProps({
+        buttonLayout: 'vertical'
+      })
+      expect(wrapper.find('.t-dialog__footer').classes().includes(selector)).toBeTruthy();
+    })
+
+    it(': width', async () => {
+      const visible = true;
+      // width 为 string 类型
+      const width = '80%';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          width,
+        },
+      });
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`width: ${width}`)).toBeTruthy();
+      // width 为 number 类型
+      const newWidth = 320;
+      await wrapper.setProps({
+        width: newWidth
+      })
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`width: ${newWidth}px`)).toBeTruthy();
+    })
+
+    it(': zIndex', async () => {
+      const visible = true;
+      const zIndex = 11500;
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          zIndex: zIndex,
+        },
+      });
+      // expect(wrapper).toMatchSnapshot()
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`z-index: ${zIndex}`)).toBeTruthy();
+      const newZIndex = 12000;
+      await wrapper.setProps({
+        zIndex: newZIndex
+      })
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`z-index: ${newZIndex}`)).toBeTruthy();
+    })
+
+    it(': closeOnOverlayClick', async () => {
+      let triggerOrigin = {};
+      const onClose = vi.fn((trigger) => {
+        triggerOrigin = trigger;
+      });
+      const onOverlayClick = vi.fn();
+      const closeOverlayClick = true;
+      const visible = true;
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          closeOverlayClick,
+          onClose,
+          onOverlayClick
+        },
+      });
+
+      const $overlay = wrapper.findComponent({ name: 't-overlay' })
+      expect($overlay.exists()).toBeTruthy();
+      $overlay.vm.$emit('click');
+      await nextTick();
+      expect(onClose).toBeCalledTimes(1)
+      expect(onOverlayClick).toBeCalledTimes(1)
+      expect(triggerOrigin).toBe('overlay')
+    })
+
+  });
+
+  describe('event', () => {
+    it(': cancel && confirm', async () => {
+      let triggerOrigin = {};
+      const visible = true;
+      const cancelBtn = 'cancel';
+      const confirmBtn = 'confirm';
+      const title = 'this is a title';
+      const onClose = vi.fn((trigger) => {
+        triggerOrigin = trigger;
+      });
+      const onCancel = vi.fn();
+      const onConfirm = vi.fn();
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+          cancelBtn,
+          confirmBtn,
+          onCancel,
+          onConfirm,
+          onClose
+        },
+      });
+      // expect(wrapper).toMatchSnapshot()
+      // footer
+      const $buttons = wrapper.findAllComponents(Button);
+      expect($buttons).toHaveLength(2);
+      expect($buttons.at(0).text()).toBe(cancelBtn);
+      expect($buttons.at(1).text()).toBe(confirmBtn);
+      $buttons.at(0).trigger('click');
+      expect(onClose).toBeCalledTimes(1)
+      expect(triggerOrigin).toBe('cancel')
+      expect(onCancel).toBeCalledTimes(1)
+      $buttons.at(1).trigger('click');
+      expect(onConfirm).toBeCalledTimes(1)
+    });
+  });
+})
diff --git a/src/drawer/__test__/index.test.jsx b/src/drawer/__test__/index.test.jsx
new file mode 100644
index 00000000..0eaec00b
--- /dev/null
+++ b/src/drawer/__test__/index.test.jsx
@@ -0,0 +1,154 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import drawer from '../drawer.vue';
+import { ref, h } from 'vue';
+import { AppIcon } from 'tdesign-icons-vue-next';
+import { async } from 'regenerator-runtime';
+
+describe('drawer', () => {
+  // test props
+  describe('props', () => {
+    it(': visible	', async () => {
+			const wrapper = mount(drawer, {
+				props: {
+					visible: true,
+				},
+      });
+
+      expect(wrapper.find('.t-overlay').classes('t-overlay--active')).toBe(true)
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(true)
+      await wrapper.setProps({
+        visible: false,
+      })
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(false)
+    })
+
+    it(': closeOnOverlayClick', async () => {
+      let triggerOrigin = {};
+      const onClose = (trigger) => {
+        triggerOrigin = trigger.trigger;
+      };
+      const closeOverlayClick = ref(true);
+
+      const visible = ref(true);
+      const wrapper = mount({
+        render() {
+          return <drawer visible={visible.value} closeOverlayClick={closeOverlayClick} onClose={onClose}/>
+        }
+      })
+
+      const $overlay = wrapper.findComponent({ name: 't-overlay' })
+      expect($overlay.exists()).toBeTruthy()
+      $overlay.vm.$emit('click')
+      expect(triggerOrigin).toBe('overlay')
+    })
+
+    it(': items', async () => {
+      const titleSidebar = ref([
+        {
+          title: '菜单一'
+        },
+        {
+          title: '菜单二'
+        }
+      ]);
+
+      const wrapper = mount({
+        render() {
+          return <drawer items={titleSidebar.value} />
+        }
+      })
+
+      const $items = wrapper.findAll('.t-drawer__sidebar-item')
+      expect($items).toHaveLength(2)
+      titleSidebar.value.map(async (item, index)=> {
+        expect($items.at(index).text()).toEqual(item.title)
+      });
+    })
+  });
+
+  // test events
+  describe('event', () => {
+    it(': overlayClick', async () => {
+      const onOverlayClick = vi.fn();
+      const visible = ref(true);
+      const wrapper = mount({
+        render() {
+          return <drawer visible={visible.value} onOverlayClick={onOverlayClick}/>
+        }
+      })
+
+      const $popup = wrapper.findComponent({ name: 't-popup' })
+      expect($popup.exists()).toBeTruthy()
+      $popup.vm.$emit('visible-change', visible.value)
+      expect($popup.emitted()['visible-change'].length).toBe(1)
+      expect($popup.emitted()['visible-change'][0]).toEqual([true])
+      expect(onOverlayClick).toBeCalledTimes(1)
+    })
+
+    it(': close', async () => {
+      const onClose = vi.fn();
+      const iconSidebar = ref([
+        {
+          title: '菜单一',
+          icon: () => h(AppIcon),
+        },
+        {
+          title: '菜单二',
+          icon: () => h(AppIcon),
+        }
+      ]);
+
+      const wrapper = mount({
+        render() {
+          return <drawer items={iconSidebar.value} onClose={onClose} />
+        }
+      })
+
+      const $popup = wrapper.findComponent({ name: 't-popup' })
+      expect($popup.exists()).toBeTruthy()
+      $popup.vm.$emit('close')
+      expect($popup.emitted().close).toBeTruthy()
+      expect($popup.emitted().close.length).toBe(1)
+      expect(onClose).toBeCalledTimes(1)
+    })
+
+    it(': itemClick', async () => {
+      let clickItemValue = {};
+      const onItemClick = (index, item, context) => {
+        clickItemValue.index = index;
+        clickItemValue.item = item;
+      };
+      const iconSidebar = ref([
+        {
+          title: '菜单一',
+          icon: () => h(AppIcon),
+        },
+        {
+          title: '菜单二',
+          icon: () => h(AppIcon),
+        }
+      ]);
+
+      const wrapper = mount({
+        render() {
+          return <drawer items={iconSidebar.value} onItemClick={ onItemClick } />
+        }
+      })
+
+      const $items = wrapper.findAll('.t-drawer__sidebar-item')
+      expect($items).toHaveLength(2)
+
+      const triggerClick = (index) => {
+        $items.at(index).trigger('click');
+        expect(clickItemValue.index).toEqual(index)
+        expect(clickItemValue.item).toEqual(iconSidebar.value[index])
+      }
+
+      iconSidebar.value.map(async (item, index)=> {
+        await triggerClick(index)
+      });
+
+    });
+  });
+})
diff --git a/src/image-viewer/__test__/__snapshots__/index.test.jsx.snap b/src/image-viewer/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..e0c64b07
--- /dev/null
+++ b/src/image-viewer/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,109 @@
+// Vitest Snapshot v1
+
+exports[`ImageViewer > function > : onIndexChange 1`] = `
+<transition-stub
+  class="t-image-viewer"
+>
+  <div
+    class="t-overlay t-overlay--active"
+    style="z-index: 1000; transition-duration: 300ms;"
+  >
+    
+    
+    <div
+      class="t-image-viewer__close-icon"
+    >
+      <svg
+        class="t-icon t-icon-close-circle-filled"
+        fill="none"
+        height="1em"
+        viewBox="0 0 16 16"
+        width="1em"
+      >
+        <path
+          d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
+          fill="currentColor"
+          fill-opacity="0.9"
+        />
+      </svg>
+    </div>
+    <div
+      class="t-swiper t-image-viewer__swipe"
+      style="overflow: hidden;"
+    >
+      <div
+        class="t-swiper__container"
+        data-is-trust="true"
+        style="flex-direction: row; transform: translateX(-0px); transition: transform 300ms;"
+      >
+        
+        
+        <div
+          class="t-swiper-item t-image-viewer__swipe-item copy-item"
+        >
+          
+          <img
+            class="t-image-viewer__image"
+            src="https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png"
+            style="transition-duration: .3s;"
+          />
+          
+        </div>
+        <div
+          class="t-swiper-item t-image-viewer__swipe-item"
+        >
+          
+          <img
+            class="t-image-viewer__image"
+            src="https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png"
+            style="transition-duration: .3s;"
+          />
+          
+        </div>
+        <div
+          class="t-swiper-item t-image-viewer__swipe-item"
+        >
+          
+          <img
+            class="t-image-viewer__image"
+            src="https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png"
+            style="transition-duration: .3s;"
+          />
+          
+        </div>
+        
+        
+        <div
+          class="t-swiper-item t-image-viewer__swipe-item copy-item"
+        >
+          
+          <img
+            class="t-image-viewer__image"
+            src="https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png"
+            style="transition-duration: .3s;"
+          />
+          
+        </div>
+      </div>
+      
+      <!--v-if-->
+      <span
+        class="t-swiper__pagination t-swiper__pagination-dots t-swiper__pagination-bottom"
+      >
+        
+        <span
+          class="t-swiper-dot"
+        />
+        <span
+          class="t-swiper-dot"
+        />
+        
+        <!--v-if-->
+      </span>
+      
+    </div>
+    
+    
+  </div>
+</transition-stub>
+`;
diff --git a/src/image-viewer/__test__/index.test.jsx b/src/image-viewer/__test__/index.test.jsx
new file mode 100644
index 00000000..2b7b8411
--- /dev/null
+++ b/src/image-viewer/__test__/index.test.jsx
@@ -0,0 +1,120 @@
+import { nextTick, ref } from 'vue';
+import { mount } from '@vue/test-utils';
+import { CloseCircleFilledIcon, AddCircleIcon } from 'tdesign-icons-vue-next';
+import { describe, it, vi } from 'vitest';
+import ImageViewer from '../image-viewer.vue';
+import { Swiper } from '../../swiper';
+import { trigger, triggerZoom } from './touch';
+
+const images = ref([
+  'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+  'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+  'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+]);
+
+describe('ImageViewer', () => {
+  describe('props', () => {
+    it(': closeBtn', async () => {
+      const wrapper = mount(<ImageViewer v-model:images={images.value} visible={true} closeBtn={false} />);
+
+      expect(wrapper.findComponent(CloseCircleFilledIcon).exists()).toBe(false);
+    });
+
+    it(': images', async () => {
+      const emptyImages = mount(<ImageViewer visible={true} />);
+      expect(emptyImages.find('.t-swiper-item').exists()).toBe(false);
+
+      const wrapper = mount(<ImageViewer v-model:images={images.value} visible={true} />);
+
+      await nextTick();
+      expect(wrapper.findAll('.t-swiper-item').length - 2).toBe(images.value.length);
+
+      images.value.pop();
+      await nextTick();
+      expect(wrapper.findAll('.t-swiper-item').length - 2).toBe(images.value.length);
+    });
+
+    it(': maxZoom', async () => {
+      const wrapper = mount(<ImageViewer v-model:images={images.value} visible={true} maxZoom={2} />);
+
+      const target = wrapper.find('.t-swiper-item');
+      triggerZoom(target, 200, 200, 'in');
+      expect(wrapper.vm.imageStyle).toMatchObject({ transform: `scale(${2}, ${2})` });
+
+      triggerZoom(target, 200, 200, 'out');
+      expect(wrapper.vm.imageStyle).not.toHaveProperty('transform');
+
+      // 测试 toggleScale
+      trigger(target, 'touchstart', 50, 50);
+      trigger(target, 'touchend', 50, 50);
+
+      await new Promise((resolve, reject) => {
+        setTimeout(() => {
+          resolve();
+        }, 100); // 小于 TAP_TIME
+      });
+      trigger(target, 'touchstart', 50, 50);
+      trigger(target, 'touchend', 50, 50);
+    });
+
+    it(': showIndex', async () => {
+      const wrapper = mount(<ImageViewer v-model:images={images.value} visible={true} showIndex={true} />);
+
+      expect(wrapper.find('.t-swiper__pagination-fraction').exists()).toBe(true);
+    });
+
+    it(': visible', async () => {
+      const visible = ref(false);
+      const wrapper = mount(<ImageViewer v-model:images={images.value} v-model:visible={visible.value} />);
+
+      expect(wrapper.findComponent(Swiper).exists()).toBe(false);
+      visible.value = true;
+    });
+  });
+
+  describe('slots', () => {
+    it(': closeBtn', () => {
+      const wrapper = mount(
+        <ImageViewer v-model:images={images.value} visible={true} closeBtn={() => <AddCircleIcon />} />,
+      );
+
+      expect(wrapper.findComponent(AddCircleIcon).exists()).toBe(true);
+    });
+  });
+
+  describe('event', () => {
+    it(': close', async () => {
+      const wrapper = mount(<ImageViewer v-model:images={images.value} visible={true} />);
+
+      await wrapper.findComponent(CloseCircleFilledIcon).trigger('click');
+      expect(wrapper.emitted()).toHaveProperty('close');
+    });
+  });
+
+  describe('function', () => {
+    it(': onClose', async () => {
+      const onClose = vi.fn();
+
+      const wrapper = mount(<ImageViewer v-model:images={images.value} visible={true} onClose={onClose} />);
+
+      await wrapper.findComponent(CloseCircleFilledIcon).trigger('click');
+      expect(onClose).toHaveBeenCalled();
+    });
+
+    it(': onIndexChange', async () => {
+      const onIndexChange = vi.fn();
+
+      const wrapper = mount(<ImageViewer v-model:images={images.value} visible={true} onIndexChange={onIndexChange} />);
+
+      const target = wrapper.find('.t-swiper__container');
+
+      await trigger(target, 'touchstart', 0, 0);
+      await trigger(target, 'touchmove', 30, 0);
+      await trigger(target, 'touchmove', 60, 0);
+      await trigger(target, 'touchmove', 120, 0);
+      await trigger(target, 'touchend', 120, 0);
+
+      expect(wrapper.element).toMatchSnapshot();
+    });
+  });
+});
diff --git a/src/image-viewer/__test__/touch.ts b/src/image-viewer/__test__/touch.ts
new file mode 100644
index 00000000..fafcd52d
--- /dev/null
+++ b/src/image-viewer/__test__/touch.ts
@@ -0,0 +1,75 @@
+/**
+ * https://github.com/vant-ui/vant/blob/dev/packages/vant/test/event.ts
+ */
+
+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,
+  };
+}
+
+// trigger pointer/touch event
+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();
+}
+
+function triggerTwoFingerTouchmove(el: Element | DOMWrapper<Element>, x: number, y: number) {
+  trigger(el, 'touchmove', -x, -y, { x, y });
+}
+
+// simulate zoom
+export function triggerZoom(el: Element | DOMWrapper<Element>, x: number, y: number, direction: 'in' | 'out' = 'in') {
+  trigger(el, 'touchstart', 0, 0, { x, y });
+
+  if (direction === 'in') {
+    triggerTwoFingerTouchmove(el, x / 4, y / 4);
+    triggerTwoFingerTouchmove(el, x / 3, y / 3);
+    triggerTwoFingerTouchmove(el, x / 2, y / 2);
+    triggerTwoFingerTouchmove(el, x, y);
+  } else if (direction === 'out') {
+    triggerTwoFingerTouchmove(el, x, y);
+    triggerTwoFingerTouchmove(el, x / 2, y / 2);
+    triggerTwoFingerTouchmove(el, x / 3, y / 3);
+    triggerTwoFingerTouchmove(el, x / 4, y / 4);
+  }
+
+  trigger(el, 'touchend', 0, 0, { touchList: [] });
+}
diff --git a/src/loading/__test__/__snapshots__/index.test.jsx.snap b/src/loading/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..6bc891a2
--- /dev/null
+++ b/src/loading/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,114 @@
+// Vitest Snapshot v1
+
+exports[`Loading.vue > props > :pause 1`] = `
+<div
+  class="t-loading"
+  style=""
+>
+  
+  
+  <svg
+    class="t-loading__gradient t-icon-loading"
+    height="1em"
+    style="animation-play-state: paused; animation-duration: 800ms;"
+    version="1.1"
+    viewBox="0 0 14 14"
+    width="1em"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <foreignobject
+      height="12"
+      width="12"
+      x="1"
+      y="1"
+    >
+      <div
+        class="t-loading__gradient-conic"
+      />
+    </foreignobject>
+  </svg>
+  
+  <!--v-if-->
+  <!---->
+  
+</div>
+`;
+
+exports[`Loading.vue > props > :reverse 1`] = `
+<div
+  class="t-loading"
+  style=""
+>
+  
+  
+  <svg
+    class="t-loading__gradient t-icon-loading"
+    height="1em"
+    style="animation-direction: reverse; animation-duration: 800ms;"
+    version="1.1"
+    viewBox="0 0 14 14"
+    width="1em"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <foreignobject
+      height="12"
+      width="12"
+      x="1"
+      y="1"
+    >
+      <div
+        class="t-loading__gradient-conic"
+      />
+    </foreignobject>
+  </svg>
+  
+  <!--v-if-->
+  <!---->
+  
+</div>
+`;
+
+exports[`spinner.vue > render > :text 1`] = `
+<span
+  class="t-loading__spinner"
+>
+  
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-1"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-2"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-3"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-4"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-5"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-6"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-7"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-8"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-9"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-10"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-11"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-12"
+  />
+  
+</span>
+`;
diff --git a/src/loading/__test__/index.test.jsx b/src/loading/__test__/index.test.jsx
new file mode 100644
index 00000000..c3550cd7
--- /dev/null
+++ b/src/loading/__test__/index.test.jsx
@@ -0,0 +1,98 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect } from 'vitest';
+import Loading from '../loading.vue';
+import Spinner from '../icon/spinner.vue';
+import { nextTick } from "vue";
+
+describe('Loading.vue', () => {
+  describe('props', () => {
+    beforeEach(() => {
+      vi.useFakeTimers()
+    })
+
+    afterEach(() => {
+      vi.restoreAllMocks()
+    })
+
+    it(':progress', () => {
+      const wrapper = mount(() => (
+          <Loading theme="bar" progress={0.5} />
+      ));
+
+      const shadow = wrapper.find('.t-loading__shadow');
+      expect(shadow.exists()).toBeTruthy();
+    });
+
+    it(':delay', async () => {
+      const wrapper = mount(() => (
+          <Loading delay={1000} />
+      ));
+      expect(wrapper.find('.t-icon-loading').exists()).not.toBeTruthy();
+      vi.runAllTimers();
+      await nextTick();
+      expect(wrapper.find('.t-icon-loading').exists()).toBeTruthy();
+    });
+
+    it(':error', () => {
+      const wrapper = mount(() => (
+          <Loading theme="error"/>
+      ));
+      const errorBlock = wrapper.find('.t-loading__text');
+      expect(errorBlock.element.innerHTML).toBe('加载失败');
+    });
+
+    it(':inheritColor', () => {
+      const wrapper = mount(Loading, {
+        props: {
+          inheritColor: true
+        },
+      });
+      expect(wrapper.vm.rootStyle).toBe('color: inherit')
+    });
+
+    it(':pause', () => {
+      const wrapper = mount(Loading, {
+        props: {
+          pause: true
+        },
+      });
+      expect(wrapper.element).toMatchSnapshot();
+      const iconWrapper = wrapper.find('.t-loading__gradient')
+      const iconWrapperStyle = iconWrapper.attributes().style;
+      expect(iconWrapperStyle).toContain('animation-play-state: paused')
+    });
+
+    it(':reverse', () => {
+      const wrapper = mount(() => (
+          <Loading reverse={true}/>
+      ));
+      expect(wrapper.element).toMatchSnapshot();
+      const iconWrapper = wrapper.find('.t-loading__gradient')
+      const iconWrapperStyle = iconWrapper.attributes().style;
+      expect(iconWrapperStyle).toContain('animation-direction: reverse')
+    });
+  })
+
+  describe('slots', () => {
+    it(':text', () => {
+      const customLoading = 'custom-loading-text'
+      const wrapper = mount(() => (
+          <Loading>{{ text: () => <p className={customLoading}>{customLoading}</p> }}</Loading>
+      ));
+      const text = wrapper.find('.t-loading__text');
+      expect(text.exists()).toBeTruthy();
+      expect(text.element.innerHTML).toBe(`<p class="${customLoading}">${customLoading}</p>`);
+    });
+  })
+})
+
+describe('spinner.vue', () => {
+  describe('render', () => {
+    it(':text', () => {
+      const wrapper = mount(() => (
+          <Spinner/>
+      ));
+      expect(wrapper.element).toMatchSnapshot();
+    });
+  })
+})
diff --git a/src/message/__test__/index.test.jsx b/src/message/__test__/index.test.jsx
new file mode 100644
index 00000000..9f8c35be
--- /dev/null
+++ b/src/message/__test__/index.test.jsx
@@ -0,0 +1,54 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect } from 'vitest';
+import { ref, nextTick } from "vue";
+import Message from '../message.vue';
+
+describe('Message.vue', () => {
+  describe('event', () => {
+    it('open', async () => {
+      const fn = vi.fn();
+      const visible = ref(false);
+      const wrapper = mount(() => (
+        <Message v-model={visible.value} content="这是一条消息通知" onOpen={fn}/>
+      ));
+      visible.value = true;
+      await nextTick();
+      expect(fn).toBeCalledTimes(1);
+      visible.value = false;
+      await nextTick();
+      expect(fn).toBeCalledTimes(1);
+    });
+
+    it('close', () => {
+      const fn = vi.fn();
+      const wrapper = mount(() => (
+        <Message visible={true} content="这是一条带关闭的消息通知，常驻可关闭" closeBtn={true} onClose={fn}/>
+      ));
+      const closeBtn = wrapper.find('.t-icon-close');
+      closeBtn.trigger('click');
+      expect(fn).toBeCalledTimes(1);
+    });
+
+    it('after-enter-leave', async () => {
+      const fn = vi.fn();
+      const wrapper = mount(() => (
+        <Message visible={true} onOpened={fn} onClosed={fn}/>
+      ));
+      expect(fn).not.toBeCalled();
+      wrapper.findComponent(Message).vm.afterEnter();
+      expect(fn).toBeCalledTimes(1);
+      wrapper.findComponent(Message).vm.afterLeave();
+      expect(fn).toBeCalledTimes(2);
+    });
+  })
+
+  describe('slots', () => {
+    it('icon:success', () => {
+      const wrapper = mount(() => (
+        <Message visible={true} content="这是一条消息通知" theme={'success'}/>
+      ));
+      const successIcon = wrapper.find('.t-icon-check-circle-filled');
+      expect(successIcon).toBeTruthy();
+    });
+  })
+});
\ No newline at end of file
diff --git a/src/overlay/__test__/index.test.jsx b/src/overlay/__test__/index.test.jsx
new file mode 100644
index 00000000..aea64ac3
--- /dev/null
+++ b/src/overlay/__test__/index.test.jsx
@@ -0,0 +1,106 @@
+import { describe, it, expect, vi } from 'vitest';
+import Overlay from '../overlay.vue'
+import { mount } from '@vue/test-utils';
+
+describe('Overlay', () => {
+  describe('props', () => {
+    it(': visible', async () => {
+      const wrapper = mount(Overlay, {
+        props: { visible: true }
+      })
+      expect(wrapper.find('.t-overlay--active').exists()).toEqual(true)
+      await wrapper.setProps({ visible: false })
+      expect(wrapper.find('.t-overlay--active').exists()).toEqual(false)
+    });
+
+    it(': preventScrollThrough', async () => {
+      const wrapper = mount(Overlay, {
+        props: { visible: true, preventScrollThrough: false }
+      })
+      const event = {
+        preventDefault: vi.fn(),
+        stopPropagation: vi.fn()
+      }
+      wrapper.find('.t-overlay').trigger('touchmove', event)
+      expect(event.preventDefault).toHaveBeenCalledTimes(0)
+      expect(event.stopPropagation).toHaveBeenCalledTimes(0)
+
+      await wrapper.setProps({
+        preventScrollThrough: true
+      })
+      wrapper.find('.t-overlay').trigger('touchmove', event)
+      expect(event.preventDefault).toHaveBeenCalledTimes(1)
+      expect(event.stopPropagation).toHaveBeenCalledTimes(1)
+    })
+
+    it(': transparent', async () => {
+      const wrapper = mount(Overlay, {
+        props: {
+          visible: true
+        }
+      })
+      expect(wrapper.find('.t-overlay--transparent').exists()).toEqual(false)
+      await wrapper.setProps({
+        transparent: true
+      })
+      expect(wrapper.find('.t-overlay--transparent').exists()).toEqual(true)
+    })
+
+    it(': duration', async () => {
+      const wrapper = mount(Overlay, {
+        props: {
+          visible: true,
+        }
+      })
+      expect(getComputedStyle(wrapper.find('.t-overlay').element)['transition-duration']).toEqual('300ms')
+      await wrapper.setProps({
+        duration: 100
+      })
+      expect(getComputedStyle(wrapper.find('.t-overlay').element)['transition-duration']).toEqual('100ms')
+    });
+
+    it (': zIndex', async () => {
+      const wrapper = mount(Overlay, {
+        props: {
+          visible: true,
+        }
+      })
+      expect(getComputedStyle(wrapper.find('.t-overlay').element)['z-index']).toEqual('1000')
+      await wrapper.setProps({
+        zIndex: 99
+      })
+      expect(getComputedStyle(wrapper.find('.t-overlay').element)['z-index']).toEqual('99')
+    })
+
+  })
+
+  describe('slots', () => {
+    it(': default', () => {
+      const wrapper = mount(Overlay, {
+        props: { visible: true },
+        slots: {
+          default: 'my content'
+        }
+      })
+      expect(wrapper.find('.t-overlay').text()).toEqual('my content')
+    })
+  })
+
+  describe('events', () => {
+    it(': click', async () => {
+      const onClick = vi.fn()
+      const wrapper = mount(Overlay, {
+        props: {
+          visible: true,
+          onClick
+        }
+      })
+      wrapper.find('.t-overlay').trigger('click')
+      expect(wrapper.emitted('click')).toHaveLength(1)
+      expect(onClick).toHaveBeenCalledTimes(1)
+      expect(onClick).toHaveBeenCalledWith({
+        e: expect.any(MouseEvent)
+      })
+    });
+  })
+})
diff --git a/src/popup/__test__/index.test.jsx b/src/popup/__test__/index.test.jsx
new file mode 100644
index 00000000..1bd74b4a
--- /dev/null
+++ b/src/popup/__test__/index.test.jsx
@@ -0,0 +1,142 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import Popup from '../popup.vue';
+import { ref } from 'vue';
+
+describe('popup', () => {
+  // test props api
+  describe('props', () => {
+    const onOpen = vi.fn();
+    it(': visible	', async () => {
+      const wrapper = mount(Popup, {
+        props: {
+          visible: false,
+          onOpen: onOpen
+        },
+      });
+
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(false)
+      expect(wrapper.find('.t-overlay').attributes('style')).toContain('display: none')
+      await wrapper.setProps({
+        visible: true,
+      })
+      expect(wrapper.vm.currentVisible).toBe(true)
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(true)
+      expect(onOpen).toBeCalledTimes(1)
+    })
+
+    it(': placement	', async () => {
+      const wrapper = mount(Popup, {
+        props: {
+          placement: 'top'
+        },
+      });
+      expect(wrapper.find('.t-popup--content').classes()).toContain(`t-popup--content-${wrapper.vm.placement}`)
+      await wrapper.setProps({
+        placement: 'center'
+      })
+      expect(wrapper.find('.t-popup--content').classes()).toContain(`t-popup--content-${wrapper.vm.placement}`)
+    })
+
+    it(': showOverlay	', async () => {
+      const wrapper = mount(Popup, {
+        props: {
+          showOverlay: false
+        },
+      });
+      expect(wrapper.find('.t-overlay').classes('t-overlay--transparent')).toBe(true)
+      await wrapper.setProps({
+        showOverlay: true
+      })
+      expect(wrapper.find('.t-overlay').classes('t-overlay--transparent')).toBe(false)
+    })
+
+    it(': zIndex', async () => {
+      const wrapper = mount(Popup, {
+        props: {
+          zIndex: 15000
+        },
+      });
+      expect(wrapper.find('.t-popup').attributes('style')).toContain('z-index: 15000')
+      await wrapper.setProps({
+        zIndex: 17000
+      })
+      expect(wrapper.find('.t-popup').attributes('style')).toContain('z-index: 17000')
+    })
+
+    it(': transitionName', async () => {
+      const wrapper = mount(Popup, {
+        props: {
+          placement: 'top'
+        },
+      });
+      expect(wrapper.vm.contentTransitionName).toBe(`slide-${wrapper.vm.placement}`)
+      await wrapper.setProps({
+        transitionName: "slide-fade"
+      })
+      expect(wrapper.vm.contentTransitionName).toBe(wrapper.vm.transitionName)
+    })
+
+    it(': customStyle', async () => {
+      const wrapper = mount(Popup, {
+        props: {
+          customStyle: 'font-size: 18px',
+          zIndex: 17000
+        },
+      });
+      expect(wrapper.find('.t-popup').attributes('style')).toContain('font-size: 18px; z-index: 17000;')
+    })
+  })
+
+  // test events
+  describe('event', () => {
+    it(': onVisibleChange', async () => {
+      const onVisibleChange = vi.fn();
+      const open= vi.fn();
+      const opened = vi.fn();
+      const close = vi.fn();
+      const closed = vi.fn();
+      const visible = ref(true);
+      const wrapper = mount({
+        render() {
+          return <Popup
+            placement="bottom"
+            transition-name="slide-fade"
+            visible={visible.value}
+            onVisibleChange={onVisibleChange}
+            onOpen={open}
+            onOpened={opened}
+            onClose={close}
+            onClosed={closed}
+          />
+        }
+      });
+
+      const $popup = wrapper.find('.t-popup');
+      const $overlay = wrapper.find('.t-overlay');
+
+      await $overlay.trigger('click');
+
+
+      expect(onVisibleChange).toBeCalledTimes(1);
+      expect(close).toBeCalledTimes(1);
+
+      const $transition = wrapper.findAllComponents({ name: 'transition' }); // => 通过 `name` 找到 transition
+      expect($transition).toHaveLength(3);
+
+      expect($transition.at(0).exists()).toBeTruthy();
+      expect($transition.at(2).exists()).toBeTruthy();
+
+      const event = {
+        preventDefault: vi.fn()
+      }
+      await $popup.trigger('touchmove', event);
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
+
+      // 理论上点击遮罩层，可以触发 transition 的 after-leave 事件，但在测试环境中并没有触发。
+      // 手动触发 afterLeave
+      wrapper.findComponent(Popup).vm.afterLeave();
+      expect(closed).toBeCalledTimes(1);
+    })
+  });
+})
diff --git a/src/progress/__test__/index.test.jsx b/src/progress/__test__/index.test.jsx
new file mode 100644
index 00000000..434926f7
--- /dev/null
+++ b/src/progress/__test__/index.test.jsx
@@ -0,0 +1,104 @@
+import { describe, it, expect, vi } from 'vitest';
+import { getBackgroundColor } from '../utils'
+import Progress from '../progress.vue'
+import { mount } from '@vue/test-utils';
+
+describe('Progress', () => {
+   // jsdom 不支持 linear-gradient 等 css 样式 https://github.com/jsdom/jsdom/issues/2166
+  //  这里对 getBackgroundColor 方法单独验证
+  describe('utils/getBackgroundColor', () => {
+    it('color types', () => {
+      expect(getBackgroundColor('red'))
+        .toEqual('red')
+
+      expect(getBackgroundColor(['#000', '#fff']))
+        .toEqual('linear-gradient( 90deg,#000,#fff )')
+
+      expect(getBackgroundColor({
+        '0%': '#f00', '100%': '#0ff'
+      }))
+        .toEqual('linear-gradient(to right, #f00 0%,#0ff 100%)')
+    })
+  })
+
+  describe('props', () => {
+    it(': color', async () => {
+      const utils = await import('../utils')
+      const wrapper = mount(Progress, {
+        props: { color: 'orange' }
+      })
+      const percentBar = wrapper.find('.t-progress__bar-percent')
+      expect(getComputedStyle(percentBar.element).backgroundColor).toBe('orange')
+
+      await wrapper.setProps({
+        color: 'red'
+      })
+      expect(getComputedStyle(percentBar.element).backgroundColor).toBe('red')
+    })
+
+    it(': label', async () => {
+      const wrapper = mount(Progress)
+      expect(wrapper.find('.t-progress__label').exists()).toEqual(true)
+      await wrapper.setProps({
+        label: false
+      })
+      expect(wrapper.find('.t-progress__label').exists()).toEqual(false)
+    })
+
+    it(': percentage', async () => {
+      const wrapper = mount(Progress, {
+        props: { percentage: 50 }
+      })
+      const percentBar = wrapper.find('.t-progress__bar-percent')
+      expect(getComputedStyle(percentBar.element).width).toBe('50%')
+      await wrapper.setProps({
+        percentage: 80
+      })
+      expect(getComputedStyle(percentBar.element).width).toBe('80%')
+    })
+
+    it(': status', async () => {
+      const wrapper = mount(Progress, {
+        props: { status: 'success' }
+      })
+
+      expect(wrapper.classes()).toContain('t-progress--status--success')
+      await wrapper.setProps({
+        status: 'error'
+      })
+      expect(wrapper.classes()).not.toContain('t-progress--status--success')
+      expect(wrapper.classes()).toContain('t-progress--status--error')
+    })
+
+    it(': strokeWidth', async () => {
+      const wrapper = mount(Progress, {
+        props: { strokeWidth: 10 }
+      })
+      expect(getComputedStyle(wrapper.find('.t-progress__bar').element).height).toEqual('10px')
+      await wrapper.setProps({
+        strokeWidth: 8
+      })
+      expect(getComputedStyle(wrapper.find('.t-progress__bar').element).height).toEqual('8px')
+    })
+
+    it(': trackColor', async () => {
+      const wrapper = mount(Progress, {
+        props: { trackColor: 'orange' }
+      })
+      expect(getComputedStyle(wrapper.find('.t-progress__bar').element).backgroundColor).toEqual('orange')
+      await wrapper.setProps({
+        trackColor: 'white'
+      })
+      expect(getComputedStyle(wrapper.find('.t-progress__bar').element).backgroundColor).toEqual('white')
+    })
+  })
+
+  describe('slots', () => {
+    it(': label', () => {
+      const wrapper = mount(Progress, {
+        slots: { label: '25%' }
+      })
+      expect(wrapper.find('.t-progress__label').text()).toEqual('25%')
+    })
+  })
+})
diff --git a/src/stepper/__test__/__snapshots__/demo.test.jsx.snap b/src/stepper/__test__/__snapshots__/demo.test.jsx.snap
index 2de506d9..ea642a0b 100644
--- a/src/stepper/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/stepper/__test__/__snapshots__/demo.test.jsx.snap
@@ -2,369 +2,292 @@
 
 exports[`Stepper > Stepper baseVue demo works fine 1`] = `
 <div
-  class="cell-base"
+  class="stepper-demo"
 >
   <div
-    class="t-cell-group"
+    class="t-cell t-cell--middle t-cell--bordered"
   >
     <div
-      class="t-cell-group__title"
+      class="t-cell__left-icon"
     >
-      
-      基础步进器
-      
+      <!--v-if-->
+      <!--v-if-->
+    </div>
+    <div
+      class="t-cell__title"
+    >
+      标题文字
+      <!--v-if-->
+      <!--v-if-->
     </div>
     <div
-      class="t-cell-group__container"
+      class="t-cell__note"
     >
       
       <div
-        class="t-cell t-cell--middle t-cell--bordered"
+        class="t-stepper"
       >
         <div
-          class="t-cell__left-icon"
-        >
-          <!--v-if-->
-          <!--v-if-->
-        </div>
-        <div
-          class="t-cell__title"
+          class="t-stepper__minus t-is-disabled"
         >
-          标题文字
-          <!--v-if-->
-          <!--v-if-->
+          <svg
+            class="t-icon t-icon-remove t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M3.5 7.35h9v1.3h-9v-1.3z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
         </div>
+        <input
+          class="t-stepper__input"
+          type="tel"
+        />
         <div
-          class="t-cell__note"
+          class="t-stepper__plus"
         >
-          
-          <div
-            class="t-stepper"
-            modelmodifiers="[object Object]"
+          <svg
+            class="t-icon t-icon-add t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
           >
-            <div
-              class="t-stepper__minus"
-            >
-              <svg
-                class="t-icon t-icon-remove t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M3.5 7.35h9v1.3h-9v-1.3z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-            <input
-              class="t-stepper__input"
-              pattern="[0-9]*"
-              type="tel"
+            <path
+              d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
+              fill="currentColor"
+              fill-opacity="0.9"
             />
-            <div
-              class="t-stepper__plus"
-            >
-              <svg
-                class="t-icon t-icon-add t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-          </div>
-          
+          </svg>
         </div>
-        <!--v-if-->
       </div>
       
     </div>
+    <!--v-if-->
   </div>
 </div>
 `;
 
 exports[`Stepper > Stepper disableInputVue demo works fine 1`] = `
 <div
-  class="cell-base"
+  class="stepper-demo"
 >
   <div
-    class="t-cell-group"
+    class="t-cell t-cell--middle t-cell--bordered"
   >
     <div
-      class="t-cell-group__title"
+      class="t-cell__left-icon"
+    >
+      <!--v-if-->
+      <!--v-if-->
+    </div>
+    <div
+      class="t-cell__title"
     >
-      
       禁止输入
-      
+      <!--v-if-->
+      <!--v-if-->
     </div>
     <div
-      class="t-cell-group__container"
+      class="t-cell__note"
     >
       
       <div
-        class="t-cell t-cell--middle t-cell--bordered"
+        class="t-stepper"
       >
         <div
-          class="t-cell__left-icon"
-        >
-          <!--v-if-->
-          <!--v-if-->
-        </div>
-        <div
-          class="t-cell__title"
+          class="t-stepper__minus t-is-disabled"
         >
-          禁止输入
-          <!--v-if-->
-          <!--v-if-->
+          <svg
+            class="t-icon t-icon-remove t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M3.5 7.35h9v1.3h-9v-1.3z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
         </div>
+        <input
+          class="t-stepper__input"
+          disabled=""
+          readonly=""
+          type="tel"
+        />
         <div
-          class="t-cell__note"
+          class="t-stepper__plus"
         >
-          
-          <div
-            class="t-stepper"
-            modelmodifiers="[object Object]"
+          <svg
+            class="t-icon t-icon-add t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
           >
-            <div
-              class="t-stepper__minus"
-            >
-              <svg
-                class="t-icon t-icon-remove t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M3.5 7.35h9v1.3h-9v-1.3z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-            <input
-              class="t-stepper__input"
-              disabled=""
-              pattern="[0-9]*"
-              readonly=""
-              type="tel"
+            <path
+              d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
+              fill="currentColor"
+              fill-opacity="0.9"
             />
-            <div
-              class="t-stepper__plus"
-            >
-              <svg
-                class="t-icon t-icon-add t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-          </div>
-          
+          </svg>
         </div>
-        <!--v-if-->
       </div>
       
     </div>
+    <!--v-if-->
   </div>
 </div>
 `;
 
 exports[`Stepper > Stepper disabledVue demo works fine 1`] = `
 <div
-  class="cell-base"
+  class="stepper-demo"
 >
   <div
-    class="t-cell-group"
+    class="t-cell t-cell--middle t-cell--bordered"
   >
     <div
-      class="t-cell-group__title"
+      class="t-cell__left-icon"
     >
-      
-      禁用状态
-      
+      <!--v-if-->
+      <!--v-if-->
     </div>
     <div
-      class="t-cell-group__container"
+      class="t-cell__title"
+    >
+      禁用
+      <!--v-if-->
+      <!--v-if-->
+    </div>
+    <div
+      class="t-cell__note"
     >
       
       <div
-        class="t-cell t-cell--middle t-cell--bordered"
+        class="t-stepper t-is-disabled"
       >
         <div
-          class="t-cell__left-icon"
-        >
-          <!--v-if-->
-          <!--v-if-->
-        </div>
-        <div
-          class="t-cell__title"
+          class="t-stepper__minus t-is-disabled"
         >
-          失效
-          <!--v-if-->
-          <!--v-if-->
+          <svg
+            class="t-icon t-icon-remove t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M3.5 7.35h9v1.3h-9v-1.3z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
         </div>
+        <input
+          class="t-stepper__input"
+          disabled=""
+          type="tel"
+        />
         <div
-          class="t-cell__note"
+          class="t-stepper__plus t-is-disabled"
         >
-          
-          <div
-            class="t-stepper t-is-disabled"
-            modelmodifiers="[object Object]"
+          <svg
+            class="t-icon t-icon-add t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
           >
-            <div
-              class="t-stepper__minus t-is-disabled"
-            >
-              <svg
-                class="t-icon t-icon-remove t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M3.5 7.35h9v1.3h-9v-1.3z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-            <input
-              class="t-stepper__input"
-              disabled=""
-              pattern="[0-9]*"
-              type="tel"
+            <path
+              d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
+              fill="currentColor"
+              fill-opacity="0.9"
             />
-            <div
-              class="t-stepper__plus t-is-disabled"
-            >
-              <svg
-                class="t-icon t-icon-add t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-          </div>
-          
+          </svg>
         </div>
-        <!--v-if-->
       </div>
       
     </div>
+    <!--v-if-->
   </div>
 </div>
 `;
 
 exports[`Stepper > Stepper eventVue demo works fine 1`] = `
 <div
-  class="cell-base"
+  class="stepper-demo"
 >
   <div
-    class="t-cell-group"
+    class="t-cell t-cell--middle t-cell--bordered"
   >
     <div
-      class="t-cell-group__title"
+      class="t-cell__left-icon"
     >
-      
-      绑定事件
-      
+      <!--v-if-->
+      <!--v-if-->
     </div>
     <div
-      class="t-cell-group__container"
+      class="t-cell__title"
+    >
+      标题文字（单位）
+      <!--v-if-->
+      <!--v-if-->
+    </div>
+    <!--v-if-->
+    <div
+      class="t-cell__right-icon"
     >
       
       <div
-        class="t-cell t-cell--middle t-cell--bordered"
+        class="t-stepper"
       >
         <div
-          class="t-cell__left-icon"
-        >
-          <!--v-if-->
-          <!--v-if-->
-        </div>
-        <div
-          class="t-cell__title"
+          class="t-stepper__minus t-is-disabled"
         >
-          标题文字
-          <!--v-if-->
-          <!--v-if-->
+          <svg
+            class="t-icon t-icon-remove t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M3.5 7.35h9v1.3h-9v-1.3z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
         </div>
+        <input
+          class="t-stepper__input"
+          type="tel"
+        />
         <div
-          class="t-cell__note"
+          class="t-stepper__plus"
         >
-          
-          <div
-            class="t-stepper"
-            modelmodifiers="[object Object]"
+          <svg
+            class="t-icon t-icon-add t-stepper__icon"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
           >
-            <div
-              class="t-stepper__minus"
-            >
-              <svg
-                class="t-icon t-icon-remove t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M3.5 7.35h9v1.3h-9v-1.3z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-            <input
-              class="t-stepper__input"
-              pattern="[0-9]*"
-              style="width: 70px;"
-              type="tel"
+            <path
+              d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
+              fill="currentColor"
+              fill-opacity="0.9"
             />
-            <div
-              class="t-stepper__plus"
-            >
-              <svg
-                class="t-icon t-icon-add t-stepper__icon"
-                fill="none"
-                height="1em"
-                viewBox="0 0 16 16"
-                width="1em"
-              >
-                <path
-                  d="M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z"
-                  fill="currentColor"
-                  fill-opacity="0.9"
-                />
-              </svg>
-            </div>
-          </div>
-          
+          </svg>
         </div>
-        <!--v-if-->
       </div>
       
     </div>
@@ -427,7 +350,7 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
             class="t-stepper"
           >
             <div
-              class="t-stepper__minus"
+              class="t-stepper__minus t-is-disabled"
             >
               <svg
                 class="t-icon t-icon-remove t-stepper__icon"
@@ -445,7 +368,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
             </div>
             <input
               class="t-stepper__input"
-              pattern="[0-9]*"
               type="tel"
             />
             <div
@@ -509,10 +431,9 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
           
           <div
             class="t-stepper"
-            modelmodifiers="[object Object]"
           >
             <div
-              class="t-stepper__minus"
+              class="t-stepper__minus t-is-disabled"
             >
               <svg
                 class="t-icon t-icon-remove t-stepper__icon"
@@ -530,7 +451,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
             </div>
             <input
               class="t-stepper__input"
-              pattern="[0-9]*"
               type="tel"
             />
             <div
@@ -572,7 +492,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
     >
       <div
         class="t-stepper t-stepper__pure"
-        modelmodifiers="[object Object]"
       >
         <div
           class="t-stepper__minus"
@@ -593,7 +512,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
         </div>
         <input
           class="t-stepper__input"
-          pattern="[0-9]*"
           type="tel"
         />
         <div
@@ -656,7 +574,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
           
           <div
             class="t-stepper t-is-disabled"
-            modelmodifiers="[object Object]"
           >
             <div
               class="t-stepper__minus t-is-disabled"
@@ -678,7 +595,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
             <input
               class="t-stepper__input"
               disabled=""
-              pattern="[0-9]*"
               type="tel"
             />
             <div
@@ -719,7 +635,7 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
         <div
           class="t-cell__title"
         >
-          禁用单位
+          禁止输入
           <!--v-if-->
           <!--v-if-->
         </div>
@@ -728,8 +644,7 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
         >
           
           <div
-            class="t-stepper t-is-disabled"
-            modelmodifiers="[object Object]"
+            class="t-stepper"
           >
             <div
               class="t-stepper__minus t-is-disabled"
@@ -751,11 +666,11 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
             <input
               class="t-stepper__input"
               disabled=""
-              pattern="[0-9]*"
+              readonly=""
               type="tel"
             />
             <div
-              class="t-stepper__plus t-is-disabled"
+              class="t-stepper__plus"
             >
               <svg
                 class="t-icon t-icon-add t-stepper__icon"
@@ -802,7 +717,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
           
           <div
             class="t-stepper"
-            modelmodifiers="[object Object]"
           >
             <div
               class="t-stepper__minus"
@@ -823,7 +737,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
             </div>
             <input
               class="t-stepper__input"
-              pattern="[0-9]*"
               type="tel"
             />
             <div
@@ -874,7 +787,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
           
           <div
             class="t-stepper"
-            modelmodifiers="[object Object]"
           >
             <div
               class="t-stepper__minus t-is-disabled"
@@ -895,7 +807,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
             </div>
             <input
               class="t-stepper__input"
-              pattern="[0-9]*"
               type="tel"
             />
             <div
@@ -926,7 +837,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
     >
       <div
         class="t-stepper t-is-disabled t-stepper__pure"
-        modelmodifiers="[object Object]"
       >
         <div
           class="t-stepper__minus t-is-disabled"
@@ -948,7 +858,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
         <input
           class="t-stepper__input"
           disabled=""
-          pattern="[0-9]*"
           type="tel"
         />
         <div
@@ -971,7 +880,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
       </div>
       <div
         class="t-stepper t-stepper__pure"
-        modelmodifiers="[object Object]"
       >
         <div
           class="t-stepper__minus"
@@ -992,7 +900,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
         </div>
         <input
           class="t-stepper__input"
-          pattern="[0-9]*"
           type="tel"
         />
         <div
@@ -1015,7 +922,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
       </div>
       <div
         class="t-stepper t-stepper__pure"
-        modelmodifiers="[object Object]"
       >
         <div
           class="t-stepper__minus t-is-disabled"
@@ -1036,7 +942,6 @@ exports[`Stepper > Stepper mobileVue demo works fine 1`] = `
         </div>
         <input
           class="t-stepper__input"
-          pattern="[0-9]*"
           type="tel"
         />
         <div
diff --git a/src/stepper/__test__/index.test.jsx b/src/stepper/__test__/index.test.jsx
new file mode 100644
index 00000000..f977ab0f
--- /dev/null
+++ b/src/stepper/__test__/index.test.jsx
@@ -0,0 +1,159 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import { nextTick } from 'vue';
+import Stepper from '../stepper.vue';
+import { ref } from 'vue';
+
+describe('stepper', () => {
+  describe('props', () => {
+    it(': disabled', async () => {
+      const wrapper = mount(Stepper, {
+          props: {
+            disabled: true,
+          },
+      });
+      expect(wrapper.classes()).toContain('t-is-disabled');
+
+      // 更新已挂载组件的 props
+      await wrapper.setProps({
+        disabled: false,
+      })
+      expect(wrapper.classes()).not.toContain('t-is-disabled');
+    });
+
+    it(': input-width', async () => {
+      const wrapper = mount(Stepper, {
+          props: {
+            inputWidth: 80,
+          },
+      });
+      const $input = wrapper.find('.t-stepper__input');
+      expect(getComputedStyle($input.element, null).width).toBe('80px');
+
+      await wrapper.setProps({
+        inputWidth: 100,
+      })
+      expect(getComputedStyle($input.element, null).width).toBe('100px');
+    });
+
+
+    it(': theme', async () => {
+      const wrapper = mount(Stepper, {});
+
+      expect(wrapper.classes()).not.toContain('t-stepper__pure')
+      await wrapper.setProps({
+        theme: 'grey',
+      })
+      expect(wrapper.classes()).toContain('t-stepper__pure')
+    })
+
+    it(': step && max && min', async () => {
+      const onChange = vi.fn();
+
+      const max = ref(3);
+      const min = ref(1);
+      const step = ref(2);
+      const value = ref(2);
+
+      const wrapper = mount({
+        render() {
+          return <Stepper v-model={value.value} step={step.value} min={min.value} max={max.value} onChange={onChange} />
+        }
+      })
+
+      const minusIcon = wrapper.find('.t-stepper__minus');
+      const plusIcon = wrapper.find('.t-stepper__plus');
+      await minusIcon.trigger('click')
+      await nextTick()
+      // value <= min，change 触发， value = min
+      expect(onChange).toBeCalledTimes(1)
+      expect(value.value).toBe(1);
+      expect(minusIcon.classes()).toContain('t-is-disabled')
+
+      await plusIcon.trigger('click')
+      await nextTick()
+      expect(onChange).toBeCalledTimes(2)
+      expect(value.value).toBe(3);
+
+      // 此时 value = max, plusIcon 为禁用态
+      expect(plusIcon.classes()).toContain('t-is-disabled')
+
+    })
+  })
+
+  describe('events', () => {
+    it(': overlimit', async () => {
+      const onChange = vi.fn();
+      const onOverlimit = vi.fn();
+      const onBlur = vi.fn();
+      const max = ref(5);
+      const step = ref(5);
+      const value = ref(2);
+      const wrapper = mount({
+        render() {
+          return <Stepper v-model={value.value} step={step.value} max={max.value} onChange={onChange} onOverlimit={onOverlimit} onBlur={ onBlur } />
+        }
+      })
+      const minusIcon = wrapper.find('.t-stepper__minus');
+      const plusIcon = wrapper.find('.t-stepper__plus');
+      /**
+       * plusIcon
+       */
+      // 当前value < max ,触发 change，
+      await plusIcon.trigger('click')
+      await nextTick()
+      expect(onChange).toBeCalledTimes(1)
+      expect(value.value).toBe(5);
+
+      // 此时 value = max，不触发 change，但触发 overlimit
+      await plusIcon.trigger('click')
+      await nextTick()
+      expect(onChange).toBeCalledTimes(1)
+      expect(onOverlimit).toBeCalledTimes(1)
+      expect(value.value).toBe(5);
+
+      /**
+       * minusIcon
+       */
+      // 此时 value = 5，
+      await minusIcon.trigger('click')
+      await nextTick()
+      expect(value.value).toBe(0);
+      expect(onChange).toBeCalledTimes(2)
+
+      //此时 value = min， 不触发 change，但触发 overlimit
+      await minusIcon.trigger('click')
+      await nextTick()
+      expect(onChange).toBeCalledTimes(2)
+      expect(onOverlimit).toBeCalledTimes(2)
+
+      // 测试 input
+      const $input = wrapper.find('.t-stepper__input');
+
+      $input.trigger('blur')
+      expect(onBlur).toHaveBeenCalled(1)
+    });
+
+    it(': blur', async () => {
+      const onChange = vi.fn();
+      const onBlur = vi.fn();
+      const value = ref(2);
+      const wrapper = mount({
+        render() {
+          return <Stepper v-model={value.value} onChange={onChange} onBlur={ onBlur } />
+        }
+      })
+
+      const $input = wrapper.find('.t-stepper__input');
+
+      // blur
+      $input.trigger('blur')
+      expect(onBlur).toHaveBeenCalled(1)
+
+      // input, 触发 change
+      $input.setValue('10');
+      expect(onChange).toBeCalled(1);
+      expect(value.value).toBe(10);
+    });
+  })
+})
diff --git a/src/tab-bar/__test__/__snapshots__/index.test.jsx.snap b/src/tab-bar/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..63a6bfcd
--- /dev/null
+++ b/src/tab-bar/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,152 @@
+// Vitest Snapshot v1
+
+exports[`TabBar > props > snapshot 1`] = `
+<div
+  class="t-tab-bar t-tab-bar--bordered t-tab-bar--fixed"
+  role="tablist"
+>
+  
+  
+  <div
+    aria-label="TabBar"
+    aria-selected="false"
+    class="t-tab-bar-item t-no-border"
+    name="label_1"
+    role="tab"
+    text="标签一"
+  >
+    <div
+      class="t-tab-bar-item__content"
+    >
+      <div
+        class="t-tab-bar-item__icon"
+      >
+        <div
+          class="t-badge"
+        >
+          <div
+            class="t-badge__inner t-badge--has-children t-badge--medium t-badge--circle"
+            style="right: 0px; top: 5px;"
+          >
+            1
+          </div>
+          
+          <svg
+            class="t-icon t-icon-app"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M11.25 1.75a3 3 0 100 6 3 3 0 000-6zm-2 3a2 2 0 114 0 2 2 0 01-4 0zM2 3a1 1 0 011-1h3.5a1 1 0 011 1v3.5a1 1 0 01-1 1H3a1 1 0 01-1-1V3zm1 0v3.5h3.5V3H3zM2 9.5a1 1 0 011-1h3.5a1 1 0 011 1V13a1 1 0 01-1 1H3a1 1 0 01-1-1V9.5zm1 0V13h3.5V9.5H3zM8.5 9.5a1 1 0 011-1H13a1 1 0 011 1V13a1 1 0 01-1 1H9.5a1 1 0 01-1-1V9.5zm1 3.5H13V9.5H9.5V13z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+          
+        </div>
+      </div>
+      <!--v-if-->
+    </div>
+    <transition-stub>
+      <!--v-if-->
+    </transition-stub>
+  </div>
+  <div
+    aria-label="TabBar"
+    aria-selected="false"
+    class="t-tab-bar-item t-no-border"
+    name="label_2"
+    role="tab"
+    text="标签二"
+  >
+    <div
+      class="t-tab-bar-item__content"
+    >
+      <div
+        class="t-tab-bar-item__icon"
+      >
+        <div
+          class="t-badge"
+        >
+          <div
+            class="t-badge__inner t-badge--has-children t-badge--medium t-badge--circle"
+            style="right: 0px; top: 5px;"
+          >
+            1
+          </div>
+          
+          <svg
+            class="t-icon t-icon-app"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M11.25 1.75a3 3 0 100 6 3 3 0 000-6zm-2 3a2 2 0 114 0 2 2 0 01-4 0zM2 3a1 1 0 011-1h3.5a1 1 0 011 1v3.5a1 1 0 01-1 1H3a1 1 0 01-1-1V3zm1 0v3.5h3.5V3H3zM2 9.5a1 1 0 011-1h3.5a1 1 0 011 1V13a1 1 0 01-1 1H3a1 1 0 01-1-1V9.5zm1 0V13h3.5V9.5H3zM8.5 9.5a1 1 0 011-1H13a1 1 0 011 1V13a1 1 0 01-1 1H9.5a1 1 0 01-1-1V9.5zm1 3.5H13V9.5H9.5V13z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+          
+        </div>
+      </div>
+      <!--v-if-->
+    </div>
+    <transition-stub>
+      <!--v-if-->
+    </transition-stub>
+  </div>
+  <div
+    aria-haspopup="true"
+    aria-label="TabBar"
+    aria-selected="false"
+    class="t-tab-bar-item t-no-border"
+    name="label_3"
+    role="tab"
+    text="此处展开"
+  >
+    <div
+      class="t-tab-bar-item__content"
+    >
+      <div
+        class="t-tab-bar-item__icon"
+      >
+        <div
+          class="t-badge"
+        >
+          <div
+            class="t-badge__inner t-badge--has-children t-badge--medium t-badge--circle"
+            style="right: 0px; top: 5px;"
+          >
+            1
+          </div>
+          
+          <svg
+            class="t-icon t-icon-app"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M11.25 1.75a3 3 0 100 6 3 3 0 000-6zm-2 3a2 2 0 114 0 2 2 0 01-4 0zM2 3a1 1 0 011-1h3.5a1 1 0 011 1v3.5a1 1 0 01-1 1H3a1 1 0 01-1-1V3zm1 0v3.5h3.5V3H3zM2 9.5a1 1 0 011-1h3.5a1 1 0 011 1V13a1 1 0 01-1 1H3a1 1 0 01-1-1V9.5zm1 0V13h3.5V9.5H3zM8.5 9.5a1 1 0 011-1H13a1 1 0 011 1V13a1 1 0 01-1 1H9.5a1 1 0 01-1-1V9.5zm1 3.5H13V9.5H9.5V13z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+          
+        </div>
+      </div>
+      <!--v-if-->
+    </div>
+    <transition-stub>
+      <!--v-if-->
+    </transition-stub>
+  </div>
+  
+  
+</div>
+`;
diff --git a/src/tab-bar/__test__/index.test.jsx b/src/tab-bar/__test__/index.test.jsx
new file mode 100644
index 00000000..ed5468cb
--- /dev/null
+++ b/src/tab-bar/__test__/index.test.jsx
@@ -0,0 +1,149 @@
+import { describe, it, expect, vi } from 'vitest';
+import { mount } from '@vue/test-utils';
+import TabBar from '../tab-bar.vue'
+import TabBarItem from '../tab-bar-item.vue'
+import { nextTick, ref } from 'vue';
+import { AppIcon as TIconApp } from 'tdesign-icons-vue-next';
+
+const list = [
+  {
+    name: 'label_1',
+    text: '标签一',
+    icon: undefined,
+    value: '1'
+  },
+  {
+    name: 'label_2',
+    text: '标签二',
+    icon: undefined,
+    value: '2'
+  },
+  {
+    name: 'label_3',
+    text: '此处展开',
+    icon: undefined,
+    value: '3',
+    subTabBar: [
+      {
+        name: 'spread_1',
+        text: '展开项一',
+        icon: undefined,
+        value: '3-1',
+      },
+      {
+        name: 'spread_2',
+        text: '展开项二',
+        icon: undefined,
+        value: '3-2',
+      },
+      {
+        name: 'spread_3',
+        text: '展开项三',
+        icon: undefined,
+        value: '3-3',
+      },
+    ],
+  },
+];
+
+describe('TabBar', () => {
+  describe('props', () => {
+    it('bordered', async () => {
+      const wrapper = mount(TabBar, {
+        shallow: true
+      })
+      expect(wrapper.classes())
+        .toContain('t-tab-bar--bordered')
+
+      await wrapper.setProps({ 'bordered': false})
+      expect(wrapper.classes())
+        .not.toContain('t-tab-bar--bordered')
+    })
+
+    it('fixed', async () => {
+      const wrapper = mount(TabBar, {
+        shallow: true
+      })
+      expect(wrapper.classes())
+        .toContain('t-tab-bar--fixed')
+
+      await wrapper.setProps({ 'fixed': false})
+      expect(wrapper.classes())
+        .not.toContain('t-tab-bar--fixed')
+    })
+
+    it('defaultValue', async () => {
+      const wrapper = mount({
+        render: () => (
+          <TabBar defaultValue={"2"}>
+            {
+              list.map((item) => {
+                return <TabBarItem {...item}>{item.text}</TabBarItem>
+              })
+            }
+          </TabBar>
+        )
+      })
+
+      expect(wrapper.findAll('.t-tab-bar-item')).toHaveLength(list.length)
+      expect(wrapper.find('[aria-selected="true"]').attributes('text')).toEqual('标签二')
+    })
+
+    it ('snapshot', () => {
+      const icon = () => <TIconApp />
+      const wrapper = mount({
+        render: () => (
+          <TabBar>
+            {
+              list.map((item) => {
+                return <TabBarItem {...item} icon={icon} badgeProps={{count: 1}} />
+              })
+            }
+          </TabBar>
+        )
+      })
+      expect(wrapper.element).toMatchSnapshot();
+    })
+  })
+
+  describe('events', () => {
+    it ('click to change', async () => {
+      const onChange = vi.fn()
+      const value = ref('1')
+      const wrapper = mount({
+        render: () => (
+          <TabBar
+            v-model={value.value}
+            onChange={onChange}
+          >
+            {
+              list.map((item) => {
+                return <TabBarItem {...item} />
+              })
+            }
+          </TabBar>
+        )
+      })
+
+      // 从 bar1 切到 bar2
+      const bar2 = wrapper.find('[name="label_2"] > .t-tab-bar-item__content')
+      expect(onChange).toHaveBeenCalledTimes(0)
+      await bar2.trigger('click')
+      expect(onChange).toHaveBeenCalledTimes(1)
+      expect(onChange).toHaveBeenLastCalledWith('2')
+
+      // 从 bar2 切到 bar3
+      const bar3 = wrapper.find('[name="label_3"] > .t-tab-bar-item__content')
+      await bar3.trigger('click')
+      expect(onChange).toHaveBeenCalledTimes(2)
+      expect(onChange).toHaveBeenLastCalledWith(['3'])
+
+      // 点击 spread
+      const bar3_1 = wrapper.find('[name="label_3"] .t-tab-bar-item__spread-item')
+      await bar3_1.trigger('click')
+      expect(onChange).toHaveBeenCalledTimes(3)
+      expect(onChange).toHaveBeenLastCalledWith(['3', '3-1'])
+      expect(wrapper.find('[name="label_3"]').find('.t-tab-bar-item__spread-item').exists()).toBe(false)
+    })
+  })
+})
diff --git a/src/toast/__test__/index.test.jsx b/src/toast/__test__/index.test.jsx
new file mode 100644
index 00000000..51b05ad7
--- /dev/null
+++ b/src/toast/__test__/index.test.jsx
@@ -0,0 +1,87 @@
+import { describe, it, expect, vi } from 'vitest';
+import Toast from '../toast.vue'
+import Overlay from '../../overlay'
+import { mount } from '@vue/test-utils';
+import { LoadingIcon } from 'tdesign-icons-vue-next';
+
+describe('Toast', () => {
+  describe('props', () => {
+    it(': direction', async () => {
+      const wrapper = mount(Toast, {
+        direction: 'row'
+      })
+      expect(wrapper.find('.t-toast').classes()).toContain('t-toast--row')
+      await wrapper.setProps({
+        direction: 'column'
+      })
+      expect(wrapper.find('.t-toast').classes()).toContain('t-toast--column')
+    })
+
+    it(': icon', async () => {
+      const icon = () => <LoadingIcon />
+
+      const wrapper = mount(Toast, {
+        props: {
+          icon
+        }
+      })
+      expect(wrapper.find('.t-toast > svg').classes()).toContain('t-icon')
+    })
+
+    it(': message', async () => {
+      const wrapper = mount(Toast, {
+        props: {
+          message: 'Loading'
+        }
+      })
+      expect(wrapper.find('.t-toast').text()).toEqual('Loading')
+    })
+
+    it(': overlayProps', async () => {
+      const wrapper = mount(Toast)
+      await wrapper.setProps({
+        overlayProps: {
+          duration: 1000
+        }
+      })
+      expect(wrapper.findComponent(Overlay).vm.duration).toEqual(1000)
+    })
+
+    it (': placement', async () => {
+      const wrapper = mount(Toast, {
+        props: {
+          placement: 'top'
+        }
+      })
+      expect(wrapper.find('.t-toast').classes()).toContain('t-toast--top')
+      await wrapper.setProps({
+        placement: 'bottom'
+      })
+      expect(wrapper.find('.t-toast').classes()).not.toContain('t-toast--top')
+      expect(wrapper.find('.t-toast').classes()).toContain('t-toast--bottom')
+    })
+
+    it(': showOverlay', async () => {
+      const wrapper = mount(Toast, {
+        props: {
+          showOverlay: true
+        }
+      })
+      expect(wrapper.findComponent(Overlay).vm.visible).toEqual(true)
+      await wrapper.setProps({
+        showOverlay: false
+      })
+      expect(wrapper.findComponent(Overlay).vm.visible).toEqual(false)
+    })
+
+    it(': theme', async () => {
+      const wrapper = mount(Toast, {
+        props: {
+          theme: 'loading'
+        }
+      })
+
+      expect(wrapper.findComponent(LoadingIcon).exists()).toEqual(true)
+    })
+  })
+})