Repository: Tencent/tdesign-mobile-vue
Pull request #899 (2023-06-25 09:47:18)

fix: remove use-emit-event function
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [x] 日常 bug 修复
- [ ] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [x] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue

<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->
无

### 💡 需求背景和解决方案

<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->
移除冗余的 useEmitEvent 方法

### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->

- break(CheckTag): 移除废弃属性 closable
- fix(Table): 修复事件 cell-click 和 row-click 返回参数错误的问题
- fix(Drawer): 修复事件 close 返回参数错误的问题
- fix(Image): 修复事件 load 和 error 返回参数错误的问题
- fix(Search): 修复事件 action-click 返回参数错误的问题
- fix(Upload): 修复事件 select-change 返回参数错误的问题

- [ ] 本条 PR 不需要纳入 Changelog

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [x] 文档已补充或无须补充
- [x] 代码演示已提供或无须提供
- [x] TypeScript 定义已补充或无须补充
- [x] Changelog 已提供或无须提供


github-actions[bot]: [<img height="96" alt="失败" src="https://user-images.githubusercontent.com/5378891/75333447-1e63a280-58c1-11ea-975d-235367fd1522.png">](https://github.com/Tencent/tdesign-mobile-vue/actions/runs/5369123154)
<!-- AUTO_PREVIEW_HOOK -->
github-actions[bot]: **Size Change:** +1.42 kB (0%) 

**Total Size:** 474 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 278 kB | +1.21 kB (0%) |
| `./dist/tdesign.min.js` | 133 kB | +212 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |
| `./dist/tdesign.css` | 36.4 kB |
| `./dist/tdesign.min.css` | 26.3 kB |

</details>


github-actions[bot]: **Size Change:** +1.44 kB (0%) 

**Total Size:** 474 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 278 kB | +1.2 kB (0%) |
| `./dist/tdesign.min.js` | 133 kB | +237 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |
| `./dist/tdesign.css` | 36.4 kB |
| `./dist/tdesign.min.css` | 26.3 kB |

</details>


github-actions[bot]: **Size Change:** +1.44 kB (0%) 

**Total Size:** 474 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 277 kB | +1.2 kB (0%) |
| `./dist/tdesign.min.js` | 133 kB | +243 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |
| `./dist/tdesign.css` | 36.4 kB |
| `./dist/tdesign.min.css` | 26.3 kB |

</details>



Patch:
diff --git a/package.json b/package.json
index 1ab98a2d..820ca859 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
     "@babel/runtime": "^7.16.5",
     "@commitlint/cli": "^12.0.1",
     "@commitlint/config-conventional": "^12.0.1",
-    "@popperjs/core": "^2.6.0",
+    "@popperjs/core": "^2.11.8",
     "@rollup/plugin-babel": "^5.2.2",
     "@rollup/plugin-commonjs": "^21.0.1",
     "@rollup/plugin-eslint": "^8.0.1",
diff --git a/site/docs.config.js b/site/docs.config.js
index 2ce45bce..d706374e 100644
--- a/site/docs.config.js
+++ b/site/docs.config.js
@@ -366,6 +366,12 @@ export default {
           path: '/mobile-vue/components/dialog',
           component: () => import('@/dialog/dialog.md'),
         },
+        {
+          title: 'Popover 弹出气泡',
+          name: 'popover',
+          path: '/mobile-vue/components/popover',
+          component: () => import('@/popover/popover.md'),
+        },
         {
           title: 'Popup 弹出层',
           name: 'popup',
diff --git a/src/_common b/src/_common
index 00688b90..c4dc2ec8 160000
--- a/src/_common
+++ b/src/_common
@@ -1,1 +1,1 @@
-Subproject commit 00688b90a423cefc41237351456586189d3c3969
+Subproject commit c4dc2ec832a3df597a436e18fcfb0d2cd7149925
diff --git a/src/action-sheet/action-sheet-grid.vue b/src/action-sheet/action-sheet-grid.vue
index 33eb0403..a925717d 100644
--- a/src/action-sheet/action-sheet-grid.vue
+++ b/src/action-sheet/action-sheet-grid.vue
@@ -37,7 +37,6 @@
 <script lang="ts">
 import { ref, defineComponent, computed } from 'vue';
 import config from '../config';
-import { useEmitEvent } from '../shared';
 import { Grid as TGrid, GridItem as TGridItem } from '../grid';
 import { Swiper as TSwiper, SwiperItem as TSwiperItem } from '../swiper';
 
@@ -56,10 +55,13 @@ export default defineComponent({
       type: Number,
       default: 8,
     },
+    selected: {
+      type: Function,
+      default: undefined,
+    },
   },
   emits: ['selected'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const gridColumn = computed(() => Math.ceil(props.count / 2));
     const pageNum = computed(() => Math.ceil(props.items.length / props.count));
     const actionItems = computed(() => {
@@ -76,7 +78,7 @@ export default defineComponent({
       [`${name}__dots`]: pageNum.value > 1,
     }));
     const handleSelected = (i: any) => {
-      emitEvent('selected', i);
+      context.emit('selected', i);
     };
 
     return {
diff --git a/src/action-sheet/action-sheet.vue b/src/action-sheet/action-sheet.vue
index 04f64bab..cc42e03b 100644
--- a/src/action-sheet/action-sheet.vue
+++ b/src/action-sheet/action-sheet.vue
@@ -18,7 +18,7 @@
 
 <script lang="ts">
 import { ref, watch, defineComponent, computed } from 'vue';
-import { useEmitEvent, useDefault } from '../shared';
+import { useDefault } from '../shared';
 import ActionSheetList from './action-sheet-list.vue';
 import ActionSheetGrid from './action-sheet-grid.vue';
 import TPopup from '../popup';
@@ -41,7 +41,6 @@ export default defineComponent({
   props: ActionSheetProps,
   emits: ['selected', 'update:modelValue', 'cancel', 'close'],
   setup(props: any, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const actionItems = computed(() => {
       return props.items.map((item: String | ActionSheetItem) => {
         if (typeof item === 'string') {
@@ -70,7 +69,6 @@ export default defineComponent({
       () => currentVisible.value,
       (val) => {
         currentVisible.value = val;
-        // emitEvent('update:modelValue', val);
       },
       {
         immediate: true,
@@ -80,16 +78,16 @@ export default defineComponent({
 
     const hide = (trigger: string) => {
       context.emit('update:modelValue', false);
-      emitEvent('close', { trigger });
+      props.onClose?.({ trigger });
     };
 
     const handleCancel = () => {
-      emitEvent('cancel');
+      props.onCancel?.();
       context.emit('update:modelValue', false);
     };
 
     const handleSelected = (index: number) => {
-      emitEvent('selected', props?.items[index], index);
+      props.onSelected?.(props?.items[index], index);
       hide('selected');
     };
 
diff --git a/src/avatar/avatar.vue b/src/avatar/avatar.vue
index a96b5b34..86a2686f 100644
--- a/src/avatar/avatar.vue
+++ b/src/avatar/avatar.vue
@@ -29,7 +29,7 @@ import config from '../config';
 import AvatarProps from './props';
 import { TdAvatarGroupProps } from './type';
 import CLASSNAMES from '../shared/constants';
-import { renderContent, renderTNode, TNode, useEmitEvent } from '../shared';
+import { renderContent, renderTNode, TNode } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-avatar`;
@@ -39,8 +39,7 @@ export default defineComponent({
   components: { TNode, TBadge, TImage },
   props: AvatarProps,
   emits: ['error'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
     const internalInstance = getCurrentInstance();
     const avatarGroupProps = inject('avatarGroup', {}) as TdAvatarGroupProps;
     const hasAvatarGroupProps = Object.keys(avatarGroupProps).length > 0;
@@ -69,7 +68,7 @@ export default defineComponent({
     });
 
     const handleImgLoadError = (e: any) => {
-      emitEvent('error', e);
+      props.onError?.(e);
     };
 
     const customBadgeProps = computed(() => ({
diff --git a/src/back-top/back-top.vue b/src/back-top/back-top.vue
index d78878a3..5eb2e644 100644
--- a/src/back-top/back-top.vue
+++ b/src/back-top/back-top.vue
@@ -10,7 +10,7 @@ import { computed, defineComponent, getCurrentInstance, h } from 'vue';
 import { useElementBounding } from '@vueuse/core';
 import { BacktopIcon as TIconBackTop } from 'tdesign-icons-vue-next';
 
-import { useEmitEvent, renderTNode, TNode } from '../shared';
+import { renderTNode, TNode } from '../shared';
 import BackTopProps from './props';
 import config from '../config';
 
@@ -21,7 +21,6 @@ export default defineComponent({
   components: { TNode },
   props: BackTopProps,
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const el = computed(() => {
       return props.target ? props.target() : window.document.documentElement;
     });
@@ -45,7 +44,7 @@ export default defineComponent({
 
     const clickBackBtn = () => {
       window.document.documentElement.scrollTop += top.value;
-      emitEvent('to-top');
+      props.onToTop?.();
     };
     return {
       name,
diff --git a/src/button/button.vue b/src/button/button.vue
index 92aac823..de2eb8ae 100644
--- a/src/button/button.vue
+++ b/src/button/button.vue
@@ -21,7 +21,7 @@
 import { computed, toRefs, defineComponent, getCurrentInstance } from 'vue';
 
 import Loading from '../loading';
-import { useEmitEvent, renderContent, renderTNode, TNode, Hover } from '../shared';
+import { renderContent, renderTNode, TNode, Hover } from '../shared';
 import ButtonProps from './props';
 import config from '../config';
 import { useFormDisabled } from '../form/hooks';
@@ -36,7 +36,6 @@ export default defineComponent({
   props: ButtonProps,
   emits: ['click'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const disabled = useFormDisabled();
     const internalInstance = getCurrentInstance();
     const buttonClass = computed(() => [
@@ -58,7 +57,7 @@ export default defineComponent({
 
     const onClick = (e: Event) => {
       if (!props.loading && !disabled.value) {
-        emitEvent('click', e);
+        props.onClick?.(e);
       } else {
         e.stopPropagation();
       }
diff --git a/src/cascader/cascader.vue b/src/cascader/cascader.vue
index ce7fcce7..953808c0 100644
--- a/src/cascader/cascader.vue
+++ b/src/cascader/cascader.vue
@@ -88,7 +88,7 @@ import { Tabs as TTabs, TabPanel as TTabPanel } from '../tabs';
 import { RadioGroup as TRadioGroup } from '../radio';
 import config from '../config';
 import TdCascaderProps from './props';
-import { useEmitEvent, useVModel, renderTNode, TNode } from '../shared';
+import { useVModel, renderTNode, TNode } from '../shared';
 import { TreeOptionData } from '../common';
 
 const { prefix } = config;
@@ -115,7 +115,6 @@ export default defineComponent({
   props: TdCascaderProps,
   emits: ['change', 'close', 'pick', 'update:modelValue', 'update:value', 'update:visible'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const { visible, value, modelValue, subTitles, options, keys } = toRefs(props);
     const open = ref(visible.value || false);
     const [cascaderValue, setCascaderValue] = useVModel(value, modelValue, props.defaultValue, props.onChange);
@@ -193,7 +192,7 @@ export default defineComponent({
         return;
       }
 
-      emitEvent('pick', { value: item[keys.value?.value ?? 'value'], index });
+      props.onPick?.({ value: item[keys.value?.value ?? 'value'], index });
 
       if (item[(keys as Ref<KeysType>).value?.children ?? 'children']?.length) {
         items[level + 1] = item[(keys as Ref<KeysType>).value?.children ?? 'children'];
@@ -203,8 +202,7 @@ export default defineComponent({
         steps.length = level + 2;
       } else {
         setCascaderValue(item[keys.value?.value ?? 'value']);
-        emitEvent(
-          'change',
+        props.onChange?.(
           item[keys.value?.value ?? 'value'],
           items.map((item, index) => toRaw(item?.[selectedIndexes[index]])),
         );
@@ -213,7 +211,7 @@ export default defineComponent({
     };
 
     watch(open, () => {
-      emitEvent('update:visible', open.value);
+      context.emit('update:visible', open.value);
     });
 
     watch(visible, () => {
@@ -221,7 +219,7 @@ export default defineComponent({
     });
 
     const close = (trigger: string) => {
-      emitEvent('close', { trigger });
+      props.onClose?.({ trigger });
     };
 
     const onVisibleChange = (visible: boolean) => {
@@ -260,7 +258,6 @@ export default defineComponent({
       selectedIndexes,
       items,
       setCascaderValue,
-      emitEvent,
       onClose,
     };
   },
diff --git a/src/cell/cell.vue b/src/cell/cell.vue
index 8c3e2a45..7821efa2 100644
--- a/src/cell/cell.vue
+++ b/src/cell/cell.vue
@@ -27,7 +27,7 @@
 <script lang="ts">
 import { computed, defineComponent, getCurrentInstance, toRefs, h } from 'vue';
 import { ChevronRightIcon } from 'tdesign-icons-vue-next';
-import { renderTNode, renderContent, TNode, useEmitEvent, Hover } from '../shared';
+import { renderTNode, renderContent, TNode, Hover } from '../shared';
 import config from '../config';
 import CellProps from './props';
 import { useFormDisabled } from '../form/hooks';
@@ -41,8 +41,7 @@ export default defineComponent({
   directives: { Hover },
   props: CellProps,
   emits: ['click'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
     const disabled = useFormDisabled();
     const internalInstance = getCurrentInstance();
     const noteContent = computed(() => renderContent(internalInstance, 'default', 'note'));
@@ -71,7 +70,7 @@ export default defineComponent({
 
     const onClick = (e: Event) => {
       if (!disabled.value) {
-        emitEvent('click', e);
+        props.onClick?.(e);
       }
     };
 
diff --git a/src/components.ts b/src/components.ts
index b14fb4f8..10aae22f 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -55,3 +55,4 @@ export { Form, FormItem } from './form';
 export { default as Icon } from './icon';
 export { default as Table } from './table';
 export { SideBar, SideBarItem } from './side-bar';
+export { default as TPopover } from './popover';
diff --git a/src/date-time-picker/date-time-picker.vue b/src/date-time-picker/date-time-picker.vue
index 42959239..6af7c7b2 100644
--- a/src/date-time-picker/date-time-picker.vue
+++ b/src/date-time-picker/date-time-picker.vue
@@ -17,13 +17,12 @@ import { ref, computed, defineComponent, toRefs, watch, nextTick } from 'vue';
 import dayjs, { Dayjs, UnitType } from 'dayjs';
 import weekday from 'dayjs/plugin/weekday';
 import customParseFormat from 'dayjs/plugin/customParseFormat';
-import isString from 'lodash/isString';
 import isArray from 'lodash/isArray';
 
 import config from '../config';
 import DateTimePickerProps from './props';
 import { getMeaningColumn } from './shared';
-import { useEmitEvent, useVModel } from '../shared';
+import { useVModel } from '../shared';
 import { Picker as TPicker } from '../picker';
 import { PickerColumn, PickerColumnItem, PickerValue, PickerContext } from '../picker/type';
 
@@ -47,8 +46,7 @@ export default defineComponent({
   components: { TPicker },
   props: DateTimePickerProps,
   emits: ['change', 'cancel', 'confirm', 'pick', 'update:modelValue', 'update:value'],
-  setup(props: any, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props: any) {
     const className = computed(() => [`${name}`]);
     const { value, modelValue } = toRefs(props);
     const [innerValue, setDateTimePickerValue] = useVModel(value, modelValue, props.defaultValue, props.onChange);
@@ -170,12 +168,12 @@ export default defineComponent({
     });
 
     const onConfirm = () => {
-      emitEvent('confirm', dayjs(curDate.value).format(props.format));
+      props.onConfirm(dayjs(curDate.value).format(props.format));
       setDateTimePickerValue(dayjs(curDate.value).format(props.format));
     };
 
     const onCancel = (context: { e: MouseEvent }) => {
-      emitEvent('cancel', { e: context.e });
+      props.onCancel?.({ e: context.e });
     };
 
     const onPick = (value: Array<PickerValue>, context: PickerContext) => {
@@ -184,7 +182,7 @@ export default defineComponent({
       const val = curDate.value.set(type as UnitType, parseInt(columns.value[column][index]?.value, 10));
 
       curDate.value = rationalize(val);
-      emitEvent('pick', rationalize(val).format(props.format));
+      props.onPick?.(rationalize(val).format(props.format));
     };
 
     watch(innerValue, (val) => {
diff --git a/src/drawer/drawer.vue b/src/drawer/drawer.vue
index 818275ab..78b01cfa 100644
--- a/src/drawer/drawer.vue
+++ b/src/drawer/drawer.vue
@@ -41,7 +41,7 @@
 import { ref, watch, toRefs, computed, defineComponent, getCurrentInstance } from 'vue';
 import TPopup from '../popup';
 import config from '../config';
-import { useEmitEvent, renderTNode, TNode } from '../shared';
+import { renderTNode, TNode } from '../shared';
 import DrawerProps from './props';
 import { DrawerItem } from './type';
 
@@ -55,7 +55,6 @@ export default defineComponent({
   emits: ['update:visible', 'itemClick', 'overlayClick'],
   setup(props, context) {
     const currentInstance = getCurrentInstance();
-    const emitEvent = useEmitEvent(props, context.emit);
     const { visible, items, placement, showOverlay } = toRefs(props);
     const open = ref(visible.value || false);
 
@@ -63,7 +62,7 @@ export default defineComponent({
     const footerNode = computed(() => renderTNode(currentInstance, 'footer'));
 
     watch(open, () => {
-      emitEvent('update:visible', open.value);
+      context.emit('update:visible', open.value);
     });
 
     watch(visible, () => {
@@ -71,17 +70,17 @@ export default defineComponent({
     });
 
     const onItemClick = (index: number, item: DrawerItem, context: { e: MouseEvent }) => {
-      emitEvent('itemClick', index, item, context);
+      props.onItemClick?.(index, item, context);
     };
 
     const onVisibleChange = (visible: boolean) => {
       if (showOverlay.value) {
-        emitEvent('overlayClick', { visible });
+        props.onOverlayClick?.({ visible });
       }
     };
 
     const onClose = () => {
-      emitEvent('close', { trigger: 'overlay' });
+      props.onClose?.('overlay');
     };
 
     return {
diff --git a/src/dropdown-menu/dropdown-item.vue b/src/dropdown-menu/dropdown-item.vue
index 3c149eed..8be0d23b 100644
--- a/src/dropdown-menu/dropdown-item.vue
+++ b/src/dropdown-menu/dropdown-item.vue
@@ -79,10 +79,10 @@ import config from '../config';
 import TButton from '../button';
 import TPopup from '../popup';
 import TCheckbox, { CheckboxGroup as TCheckboxGroup } from '../checkbox';
-import { useVModel, useEmitEvent, uniqueFactory } from '../shared';
+import { useVModel, uniqueFactory } from '../shared';
 import DropdownItemProps from './dropdown-item-props';
 import { DropdownMenuState, DropdownMenuControl } from './context';
-import { TdDropdownMenuProps, DropdownOption, DropdownValue } from './type';
+import { TdDropdownMenuProps, DropdownValue } from './type';
 
 const { prefix } = config;
 const name = `${prefix}-dropdown-item`;
@@ -93,8 +93,7 @@ export default defineComponent({
   components: { TRadio, TButton, TPopup, TCheckbox, TRadioGroup, TCheckboxGroup },
   props: DropdownItemProps,
   emits: ['change', 'open', 'opened', 'close', 'closed', 'update:value', 'update:modelValue'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
     // 受控 value 属性
     const { value, modelValue } = toRefs(props);
     const [passInValue, setValue] = useVModel(value, modelValue, props.defaultValue);
@@ -158,7 +157,7 @@ export default defineComponent({
         top: `${bottom}px`,
       };
       const { duration } = menuProps;
-      emitEvent(val ? 'open' : 'close');
+      props[`on${val ? 'Open' : 'Close'}`]?.();
       // 动画状态控制
       if (val) {
         state.wrapperVisible = true;
@@ -172,7 +171,7 @@ export default defineComponent({
         }, Number(duration));
       }
       setTimeout(() => {
-        emitEvent(val ? 'opened' : 'closed');
+        props[`on${val ? 'Opened' : 'Closed'}`]?.();
       }, Number(duration));
     };
 
diff --git a/src/dropdown-menu/dropdown-menu.vue b/src/dropdown-menu/dropdown-menu.vue
index 907f0c6e..aec95ea3 100644
--- a/src/dropdown-menu/dropdown-menu.vue
+++ b/src/dropdown-menu/dropdown-menu.vue
@@ -13,6 +13,8 @@
 <script lang="ts">
 import { defineComponent, computed, toRefs, ref, reactive, watch, provide } from 'vue';
 import { CaretDownSmallIcon } from 'tdesign-icons-vue-next';
+import camelCase from 'lodash/camelCase';
+
 import config from '../config';
 import {
   context as menuContext,
@@ -21,7 +23,7 @@ import {
   DropdownMenuExpandState,
   TriggerSource,
 } from './context';
-import { useEmitEvent, useExpose } from '../shared';
+import { useExpose } from '../shared';
 import { findRelativeRect, findRelativeContainer } from './dom-utils';
 import DropdownMenuProps from './props';
 
@@ -33,8 +35,7 @@ export default defineComponent({
   components: { CaretDownSmallIcon },
   props: DropdownMenuProps,
   emits: ['menuOpened', 'menuClosed'],
-  setup(props, { slots, emit }) {
-    const emitEvent = useEmitEvent(props, emit);
+  setup(props, { slots }) {
     // 菜单状态
     const state = reactive<DropdownMenuState>({
       activeId: null,
@@ -97,10 +98,6 @@ export default defineComponent({
       },
     ]);
 
-    const emitEvents = (emit: string, trigger?: TriggerSource) => {
-      emitEvent(emit, { trigger });
-    };
-
     // 展开对应项目的菜单
     const expandMenu = (item: any, idx: number) => {
       const { disabled } = item;
@@ -110,10 +107,10 @@ export default defineComponent({
       if (state.activeId === idx) {
         // 再次点击时收起
         collapseMenu();
-        emitEvents('menuClosed', 'menu');
+        props.onMenuClosed?.({ trigger: 'menu' });
         return;
       }
-      emitEvents('menuOpened');
+      props.onMenuOpened?.('menuOpened');
       state.activeId = idx;
 
       // 获取菜单定位
@@ -138,7 +135,13 @@ export default defineComponent({
       const container = findRelativeContainer(bar) || document.body;
       menuContext.recordMenuExpanded(container, control, DropdownMenuExpandState.collapsed);
     };
-    const control: DropdownMenuControl = { expandMenu, collapseMenu, emitEvents };
+    const control: DropdownMenuControl = {
+      expandMenu,
+      collapseMenu,
+      emitEvents(emit: string, trigger?: TriggerSource) {
+        props[`on${camelCase(emit)}`]?.(trigger);
+      },
+    };
     // 提供子组件访问
     provide('dropdownMenuControl', control);
     useExpose({
diff --git a/src/fab/fab.vue b/src/fab/fab.vue
index cb69a439..66df0f34 100644
--- a/src/fab/fab.vue
+++ b/src/fab/fab.vue
@@ -16,7 +16,7 @@
 <script lang="ts">
 import { computed, defineProps, defineEmits, getCurrentInstance } from 'vue';
 
-import { renderTNode, useEmitEvent } from '../shared';
+import { renderTNode } from '../shared';
 import fabProps from './props';
 import config from '../config';
 import TButton from '../button';
@@ -35,11 +35,10 @@ const emit = defineEmits(['click']);
 
 const internalInstance = getCurrentInstance();
 const iconTNode = computed(() => renderTNode(internalInstance, 'icon'));
-const emitEvent = useEmitEvent(props, emit);
 
 const classes = computed(() => ({
   [`${name}`]: true,
 }));
 
-const onClick = (e: MouseEvent) => emitEvent('click', { e });
+const onClick = (e: MouseEvent) => props.onClick?.({ e });
 </script>
diff --git a/src/image-viewer/image-viewer.vue b/src/image-viewer/image-viewer.vue
index 2b889956..f26e7f8b 100644
--- a/src/image-viewer/image-viewer.vue
+++ b/src/image-viewer/image-viewer.vue
@@ -41,7 +41,7 @@ import { CloseIcon, DeleteIcon } from 'tdesign-icons-vue-next';
 
 import config from '../config';
 import ImagediverProps from './props';
-import { renderTNode, TNode, useEmitEvent, useDefault, useTouch } from '../shared';
+import { renderTNode, TNode, useDefault, useTouch } from '../shared';
 import { TdImageViewerProps } from './type';
 
 // inner components
@@ -64,19 +64,18 @@ export default defineComponent({
     TImage,
   },
   props: ImagediverProps,
-  emits: ['close', 'index-change', 'update:visible', 'update:modelValue', 'change'],
-  setup(props, context) {
+  emits: ['close', 'index-change', 'update:visible', 'update:modelValue', 'delete'],
+  setup(props, { emit }) {
     const internalInstance = getCurrentInstance();
     const state = reactive({
       zooming: false,
       scale: 1,
       swiperStyle: [] as string[],
     });
-    const emitEvent = useEmitEvent(props, context.emit);
-    const [visible, setVisible] = useDefault(props, context.emit, 'visible', 'change');
+    const [visible, setVisible] = useDefault(props, emit, 'visible', 'change');
     const [currentIndex, setIndex] = useDefault<TdImageViewerProps['index'], TdImageViewerProps>(
       props,
-      context.emit,
+      emit,
       'index',
       'index-change',
     );
@@ -109,11 +108,11 @@ export default defineComponent({
 
     const handleClose = (e: Event, trigger: string) => {
       setVisible(false);
-      emitEvent('close', { trigger, e });
+      emit('close', { trigger, e });
     };
 
     const handleDelete = () => {
-      emitEvent('delete', currentIndex);
+      emit('delete', currentIndex.value ?? 0);
     };
 
     const onSwiperChange = (index: number, context: any) => {
diff --git a/src/image/image.vue b/src/image/image.vue
index d2a8e26d..a22e2e2a 100644
--- a/src/image/image.vue
+++ b/src/image/image.vue
@@ -26,7 +26,7 @@ import { useIntersectionObserver } from '@vueuse/core';
 import { CloseIcon } from 'tdesign-icons-vue-next';
 
 import Loading from '../loading';
-import { useEmitEvent, renderTNode, TNode } from '../shared';
+import { renderTNode, TNode } from '../shared';
 import ImageProps from './props';
 import config from '../config';
 
@@ -38,8 +38,6 @@ export default defineComponent({
   components: { TNode },
   props: ImageProps,
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
-
     // 默认loading和error状态展示，slot支持Node和Function
     const internalInstance = getCurrentInstance();
 
@@ -93,7 +91,7 @@ export default defineComponent({
 
     // 图片加载完成回调
     const handleImgLoadCompleted = (e: Event) => {
-      emitEvent('load', e);
+      props.onLoad?.({ e });
       isLoading.value = false;
     };
 
@@ -102,7 +100,7 @@ export default defineComponent({
       if (realSrc.value === '') {
         return;
       }
-      emitEvent('error', e);
+      props.onError?.({ e });
       isLoading.value = false;
       isError.value = true;
     };
diff --git a/src/indexes/indexes.vue b/src/indexes/indexes.vue
index d3568c3e..992be622 100644
--- a/src/indexes/indexes.vue
+++ b/src/indexes/indexes.vue
@@ -39,7 +39,6 @@ import {
 import _ from 'lodash';
 import config from '../config';
 import IndexesProps from './props';
-import { useEmitEvent } from '../shared';
 
 const { prefix } = config;
 
@@ -66,8 +65,7 @@ export default defineComponent({
   name: componentName,
   props: IndexesProps,
   emits: ['select', 'change'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
     let timeOut: number;
     const indexesRoot = ref<HTMLElement>();
     const parentRect = ref();
@@ -148,7 +146,7 @@ export default defineComponent({
     };
 
     const handleSidebarItemClick = (index: string | number) => {
-      emitEvent('select', index);
+      props.onSelect?.(index);
       setActiveSidebarAndTip(index);
       scrollToByIndex(index);
     };
@@ -214,7 +212,7 @@ export default defineComponent({
       () => state.activeSidebar,
       (val, oldVal) => {
         if (val !== oldVal) {
-          emitEvent('change', state.activeSidebar);
+          props.onChange?.(state.activeSidebar);
         }
       },
     );
diff --git a/src/input/input.vue b/src/input/input.vue
index 14fcb750..bc69a983 100644
--- a/src/input/input.vue
+++ b/src/input/input.vue
@@ -56,7 +56,7 @@ import { useFocus } from '@vueuse/core';
 import config from '../config';
 import InputProps from './props';
 import { InputValue, TdInputProps } from './type';
-import { useEmitEvent, getCharacterLength, renderTNode, TNode, useDefault, extendAPI } from '../shared';
+import { getCharacterLength, renderTNode, TNode, useDefault, extendAPI } from '../shared';
 import { useFormDisabled } from '../form/hooks';
 
 const { prefix } = config;
@@ -77,7 +77,6 @@ export default defineComponent({
   },
   emits: ['update:value', 'update:modelValue', 'click-icon', 'focus', 'blur', 'change', 'clear'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const disabled = useFormDisabled();
 
     const inputRef = ref();
@@ -161,13 +160,13 @@ export default defineComponent({
     const handleClear = (e: MouseEvent) => {
       innerValue.value = '';
       focused.value = true;
-      emitEvent('clear', { e });
+      props.onClear?.({ e });
     };
     const handleFocus = (e: FocusEvent) => {
-      emitEvent('focus', innerValue.value, { e });
+      props.onFocus?.(innerValue.value, { e });
     };
     const handleBlur = (e: FocusEvent) => {
-      emitEvent('blur', innerValue.value, { e });
+      props.onBlur?.(innerValue.value, { e });
     };
 
     const handleCompositionend = (e: InputEvent | CompositionEvent) => {
diff --git a/src/link/link.vue b/src/link/link.vue
index be8ce71b..74d2ac9b 100644
--- a/src/link/link.vue
+++ b/src/link/link.vue
@@ -22,7 +22,7 @@
 import { defineComponent, computed, getCurrentInstance } from 'vue';
 import config from '../config';
 import LinkProps from './props';
-import { useEmitEvent, renderContent, renderTNode, TNode } from '../shared';
+import { renderContent, renderTNode, TNode } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-link`;
@@ -33,8 +33,8 @@ export default defineComponent({
     TNode,
   },
   props: LinkProps,
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
+    const baseClass = name;
     const _this = getCurrentInstance();
     const linkContent = computed(() => renderContent(_this, 'default', 'content'));
     const prefixContent = computed(() => renderTNode(_this, 'prefixIcon'));
@@ -42,7 +42,7 @@ export default defineComponent({
 
     const handleClick = (e: MouseEvent) => {
       if (props.disabled) return;
-      emitEvent('click', e);
+      props.onClick?.(e);
     };
 
     const linkClass = computed(() => [
diff --git a/src/list/list.vue b/src/list/list.vue
index d2f9876b..33c712c6 100644
--- a/src/list/list.vue
+++ b/src/list/list.vue
@@ -20,7 +20,7 @@ import { useWindowSize, useEventListener } from '@vueuse/core';
 import TLoading from '../loading';
 import config from '../config';
 import ListProps from './props';
-import { renderTNode, TNode, useScrollParent, useEmitEvent } from '../shared';
+import { renderTNode, TNode, useScrollParent } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-list`;
@@ -38,7 +38,6 @@ export default defineComponent({
   props: ListProps,
   emits: ['load-more', 'scroll'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const root = ref<HTMLElement>();
     const empty = ref<HTMLElement>();
     const scrollParent = useScrollParent(root);
@@ -50,7 +49,7 @@ export default defineComponent({
 
     const onLoadMore = (e: MouseEvent) => {
       if (props.asyncLoading === 'load-more') {
-        emitEvent('load-more');
+        props.onLoadMore?.();
       }
     };
 
@@ -62,7 +61,7 @@ export default defineComponent({
         (e.target as HTMLElement).scrollTop || document.documentElement.scrollTop || document.body.scrollTop;
       const offsetHeight = (e.target as HTMLElement).offsetHeight || height.value;
 
-      emitEvent('scroll', scrollHeight - (scrollTop + offsetHeight), scrollTop);
+      props.onScroll?.(scrollHeight - (scrollTop + offsetHeight), scrollTop);
     };
 
     useEventListener(scrollParent, 'scroll', handleScroll);
diff --git a/src/message/message.vue b/src/message/message.vue
index 4e358ab6..89bd8f45 100644
--- a/src/message/message.vue
+++ b/src/message/message.vue
@@ -35,9 +35,9 @@ import {
 import { CheckCircleFilledIcon, CloseIcon, InfoCircleFilledIcon } from 'tdesign-icons-vue-next';
 import { isFunction } from 'lodash';
 import messageProps from './props';
-import { DrawMarquee } from './type';
+import { DrawMarquee, TdMessageProps } from './type';
 import config from '../config';
-import { renderContent, renderTNode, TNode, useEmitEvent, useVModel } from '../shared';
+import { renderContent, renderTNode, TNode, useVModel } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-message`;
@@ -53,8 +53,7 @@ export default defineComponent({
   components: { TNode },
   props: messageProps,
   emits: ['durationEnd', 'closeBtnClick'],
-  setup(props: any, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props, context) {
     const internalInstance = getCurrentInstance();
 
     // 初始化动画相关数据
@@ -84,13 +83,13 @@ export default defineComponent({
       [`${name}__text-nowrap`]: props.marquee,
     }));
 
-    const changeNumToStr = (arr: []) => {
+    const changeNumToStr = (arr: TdMessageProps['offset'] = []) => {
       return arr.map(function (item) {
         return typeof item === 'number' ? `${item}px` : item;
       });
     };
 
-    const getMessageStylesOffset = (offset: []) => {
+    const getMessageStylesOffset = (offset: TdMessageProps['offset']) => {
       const arr = changeNumToStr(offset);
       return {
         top: arr[0],
@@ -153,11 +152,11 @@ export default defineComponent({
       state.scroll = {
         marquee: true,
         // 负数统一当作循环播放
-        loop: Math.max(props.marquee?.loop, -1) || loop,
+        loop: Math.max(props.marquee === true || props.marquee?.loop == null ? loop : props.marquee?.loop, -1),
         // 速度必须为正数
-        speed: Math.max(props.marquee?.speed, 1) || speed,
+        speed: Math.max(props.marquee === true || props.marquee?.speed == null ? speed : props.marquee?.speed, 1),
         // 延迟不可为负数
-        delay: Math.max(props.marquee?.delay, 0) || delay,
+        delay: Math.max(props.marquee === true || props.marquee?.delay == null ? delay : props.marquee?.delay, 0),
       };
       state.offset = 0;
 
@@ -198,14 +197,14 @@ export default defineComponent({
     };
 
     const onCloseBtnClick = () => {
-      emitEvent('closeBtnClick');
+      props.onCloseBtnClick?.();
       setVisible(false);
     };
 
     const handleDuration = () => {
       if (props.duration > 0) {
         setTimeout(() => {
-          emitEvent('durationEnd');
+          props.onDurationEnd?.();
           onCloseBtnClick();
         }, props.duration);
       }
diff --git a/src/navbar/navbar.vue b/src/navbar/navbar.vue
index 92e5673b..9e9189e4 100644
--- a/src/navbar/navbar.vue
+++ b/src/navbar/navbar.vue
@@ -24,7 +24,7 @@
 import { computed, defineComponent, getCurrentInstance, ref, toRefs } from 'vue';
 import { ChevronLeftIcon as TChevronLeftIcon } from 'tdesign-icons-vue-next';
 import config from '../config';
-import { renderTNode, TNode, useEmitEvent } from '../shared';
+import { renderTNode, TNode } from '../shared';
 import NavbarProps from './props';
 
 const { prefix } = config;
@@ -70,14 +70,12 @@ export default defineComponent({
     const rightContent = computed(() => renderTNode(internalInstance, 'right'));
     const capsuleContent = computed(() => renderTNode(internalInstance, 'capsule'));
 
-    const emitEvent = useEmitEvent(props, context.emit);
-
     const handleLeftClick = () => {
-      emitEvent('left-click');
+      props.onLeftClick?.();
     };
 
     const handleRightClick = () => {
-      emitEvent('right-click');
+      props.onRightClick?.();
     };
 
     return {
diff --git a/src/notice-bar/notice-bar.vue b/src/notice-bar/notice-bar.vue
index d80a025b..87d4c5ac 100644
--- a/src/notice-bar/notice-bar.vue
+++ b/src/notice-bar/notice-bar.vue
@@ -65,7 +65,7 @@ import { Swiper as TSwiper, SwiperItem as TSwiperItem } from '../swiper';
 import NoticeBarProps from './props';
 import { NoticeBarTrigger, DrawMarquee } from './type';
 import config from '../config';
-import { useEmitEvent, renderTNode, TNode, useVModel } from '../shared';
+import { renderTNode, TNode, useVModel } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-notice-bar`;
@@ -81,7 +81,6 @@ export default defineComponent({
   props: NoticeBarProps,
   emits: ['click'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const internalInstance = getCurrentInstance();
     // 初始化数据
     const state = reactive({
@@ -123,7 +122,7 @@ export default defineComponent({
     const showContent = computed(() => renderTNode(internalInstance, 'content'));
     // click
     function handleClick(trigger: NoticeBarTrigger) {
-      emitEvent('click', trigger);
+      props.onClick?.(trigger);
     }
     // 动画 i
     const animateStyle = computed(() => ({
diff --git a/src/picker/picker-item.vue b/src/picker/picker-item.vue
index 053babe8..c36e6cd1 100644
--- a/src/picker/picker-item.vue
+++ b/src/picker/picker-item.vue
@@ -11,7 +11,6 @@ import { ref, computed, onMounted, toRefs, defineComponent, PropType, watch } fr
 import config from '../config';
 import Picker from './picker.class';
 import { PickerColumnItem, PickerValue } from './type';
-import { useEmitEvent } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-picker-item`;
@@ -30,10 +29,13 @@ export default defineComponent({
       type: Function,
       default: undefined,
     },
+    onPick: {
+      type: Function,
+      default: undefined,
+    },
   },
   emits: ['pick'],
-  setup(props: any, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props, context) {
     let picker: Picker | null = null;
     const el = document.createElement('ul');
     const root = ref(el);
@@ -84,7 +86,7 @@ export default defineComponent({
         onChange: (index: number) => {
           const curItem = props.options[index];
           const changeValue = { value: curItem.value, index };
-          emitEvent('pick', changeValue);
+          props.onPick?.(changeValue);
         },
       });
     });
diff --git a/src/picker/picker.vue b/src/picker/picker.vue
index 8ec256e7..f6c542a8 100644
--- a/src/picker/picker.vue
+++ b/src/picker/picker.vue
@@ -23,14 +23,14 @@
 </template>
 
 <script lang="ts">
-import { computed, nextTick, defineComponent, toRefs, onMounted, ref, getCurrentInstance, watch } from 'vue';
+import { computed, defineComponent, toRefs, onMounted, ref, getCurrentInstance, watch } from 'vue';
 import isString from 'lodash/isString';
 import isBoolean from 'lodash/isBoolean';
 
 import config from '../config';
 import PickerProps from './props';
 import { PickerValue, PickerColumn, PickerColumnItem } from './type';
-import { useEmitEvent, useVModel, useChildSlots, useExpose, TNode, renderTNode } from '../shared';
+import { useVModel, useChildSlots, TNode, renderTNode } from '../shared';
 import PickerItem from './picker-item.vue';
 
 const { prefix } = config;
@@ -45,9 +45,8 @@ export default defineComponent({
   components: { PickerItem, TNode },
   props: PickerProps,
   emits: ['change', 'cancel', 'pick', 'update:modelValue', 'update:value'],
-  setup(props: any, context) {
+  setup(props: any) {
     const internalInstance = getCurrentInstance();
-    const emitEvent = useEmitEvent(props, context.emit);
     const { value, modelValue } = toRefs(props);
     const [pickerValue = ref([]), setPickerValue] = useVModel(value, modelValue, props.defaultValue, props.onChange);
     const getDefaultText = (prop: string | boolean, defaultText: string): string => {
@@ -80,13 +79,13 @@ export default defineComponent({
       const label = target.map((item: PickerColumnItem) => item.label);
       const value = target.map((item: PickerColumnItem) => item.value);
       setPickerValue(value);
-      emitEvent('confirm', value, { index: curIndexArray, label, e });
+      props.onConfirm?.(value, { index: curIndexArray, label, e });
     };
     const handleCancel = (e: MouseEvent) => {
       pickerItemInstanceArray.value.forEach((item: any, index: number) => {
         item.exposed?.setIndex(curIndexArray[index]);
       });
-      emitEvent('cancel', { e });
+      props.onCancel?.({ e });
     };
     const handlePick = (context: any, column: number) => {
       const { index } = context;
@@ -94,7 +93,7 @@ export default defineComponent({
       curIndexArray[column] = index;
       curValueArray.value[column] = realColumns.value[column][index]?.value;
 
-      emitEvent('pick', curValueArray.value, { index, column });
+      props.onPick?.(curValueArray.value, { index, column });
     };
 
     watch(pickerValue, () => {
diff --git a/src/popover/demos/mobile.vue b/src/popover/demos/mobile.vue
new file mode 100644
index 00000000..8eabbf0b
--- /dev/null
+++ b/src/popover/demos/mobile.vue
@@ -0,0 +1,26 @@
+<template>
+  <div class="tdesign-mobile-demo">
+    <h1 class="title">Popover 弹出气泡</h1>
+    <p class="summary">用于文字提示的气泡框。</p>
+    <tdesign-demo-block title="01 组件类型">
+      <typeDemo />
+    </tdesign-demo-block>
+    <tdesign-demo-block title="02 组件样式">
+      <themeDemo />
+    </tdesign-demo-block>
+    <tdesign-demo-block>
+      <placementDemo />
+    </tdesign-demo-block>
+  </div>
+</template>
+<script lang="ts" setup>
+import typeDemo from './type.vue';
+import themeDemo from './theme.vue';
+import placementDemo from './placement.vue';
+</script>
+
+<style lang="less" scoped>
+.tdesign-mobile-demo {
+  background-color: #fff;
+}
+</style>
diff --git a/src/popover/demos/placement.vue b/src/popover/demos/placement.vue
new file mode 100644
index 00000000..27314853
--- /dev/null
+++ b/src/popover/demos/placement.vue
@@ -0,0 +1,250 @@
+<template>
+  <div class="popover-example">
+    <div class="popover-example__summary mb-16">顶部弹出气泡</div>
+    <div class="popover-example__content row mb-24">
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[0]" placement="top-left" theme="dark" content="弹出气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-width--small"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(0)"
+            >
+              顶部左
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[1]" placement="top" theme="dark" content="弹出气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-width--small"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(1)"
+            >
+              顶部中
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[2]" placement="top-right" theme="dark" content="弹出气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-width--small"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(2)"
+            >
+              顶部右
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+    </div>
+  </div>
+
+  <div class="popover-example">
+    <div class="popover-example__summary mb-16">底部弹出气泡</div>
+    <div class="popover-example__content row mb-24">
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[3]" placement="bottom-left" theme="dark" content="弹出气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-width--small"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(3)"
+            >
+              底部左
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[4]" placement="bottom" theme="dark" content="弹出气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-width--small"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(4)"
+            >
+              底部中
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[5]" placement="bottom-right" theme="dark" content="弹出气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-width--small"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(5)"
+            >
+              底部右
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+    </div>
+  </div>
+
+  <div class="popover-example">
+    <div class="popover-example__summary mb-16">右侧弹出气泡</div>
+    <div class="popover-example__content column mb-24">
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[6]" placement="right-top" theme="dark" content="气泡内容">
+          <template #triggerElement>
+            <t-button class="button-with--large" theme="primary" variant="outline" size="large" @click="handleClick(6)">
+              右侧上
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[7]" placement="right" theme="dark" content="气泡内容">
+          <template #triggerElement>
+            <t-button class="button-with--large" theme="primary" variant="outline" size="large" @click="handleClick(7)">
+              右侧中
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[8]" placement="right-bottom" theme="dark" content="气泡内容">
+          <template #triggerElement>
+            <t-button class="button-with--large" theme="primary" variant="outline" size="large" @click="handleClick(8)">
+              右侧下
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+    </div>
+  </div>
+
+  <div class="popover-example">
+    <div class="popover-example__summary mb-16">左侧弹出气泡</div>
+    <div class="popover-example__content column flex-end mb-24">
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[9]" placement="left-top" theme="dark" content="气泡内容">
+          <template #triggerElement>
+            <t-button class="button-with--large" theme="primary" variant="outline" size="large" @click="handleClick(9)">
+              左侧上
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[10]" placement="left" theme="dark" content="气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-with--large"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(10)"
+            >
+              左侧中
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+      <div class="popover-example__content__item">
+        <t-popover :visible="visibleList[11]" placement="left-bottom" theme="dark" content="气泡内容">
+          <template #triggerElement>
+            <t-button
+              class="button-with--large"
+              theme="primary"
+              variant="outline"
+              size="large"
+              @click="handleClick(11)"
+            >
+              左侧下
+            </t-button>
+          </template>
+        </t-popover>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const visibleList = ref([false, false, false, false, false, false, false, false, false, false, false, false]);
+
+const handleClick = (number: number) => {
+  const newVisibleList = visibleList.value.map((item, index) => {
+    if (index === number) {
+      return !item;
+    }
+    return false;
+  });
+  visibleList.value = newVisibleList;
+};
+</script>
+
+<style lang="less" scoped>
+.popover-example {
+  padding: 0 16px;
+
+  &__summary {
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.6);
+    line-height: 22px;
+  }
+
+  &__content {
+    display: flex;
+
+    &__item {
+      flex: 1;
+    }
+  }
+
+  .row {
+    .popover-example__content__item:not(:last-child) {
+      margin-right: 16px;
+    }
+  }
+
+  .column {
+    flex-direction: column;
+
+    .popover-example__content__item:not(:last-child) {
+      margin-bottom: 16px;
+    }
+  }
+
+  .flex-end {
+    align-items: flex-end;
+  }
+}
+
+.button-width--small {
+  width: 102px;
+}
+
+.button-with--large {
+  width: 223px;
+}
+
+.mb-24 {
+  margin-bottom: 24px;
+}
+
+.mb-16 {
+  margin-bottom: 16px;
+}
+</style>
diff --git a/src/popover/demos/theme.vue b/src/popover/demos/theme.vue
new file mode 100644
index 00000000..b1ec48f8
--- /dev/null
+++ b/src/popover/demos/theme.vue
@@ -0,0 +1,106 @@
+<template>
+  <div class="popover-example">
+    <div class="popover-example__content">
+      <t-popover :visible="visibleList[0]" placement="top" theme="dark" content="弹出气泡内容">
+        <template #triggerElement>
+          <t-button class="button-width--small" theme="primary" variant="outline" size="large" @click="handleClick(0)">
+            深色
+          </t-button>
+        </template>
+      </t-popover>
+    </div>
+    <div class="popover-example__content">
+      <t-popover :visible="visibleList[1]" placement="top" theme="light" content="弹出气泡内容">
+        <template #triggerElement>
+          <t-button class="button-width--small" theme="primary" variant="outline" size="large" @click="handleClick(1)">
+            浅色
+          </t-button>
+        </template>
+      </t-popover>
+    </div>
+    <div class="popover-example__content">
+      <t-popover :visible="visibleList[2]" placement="top" theme="brand" content="弹出气泡内容">
+        <template #triggerElement>
+          <t-button class="button-width--small" theme="primary" variant="outline" size="large" @click="handleClick(2)">
+            品牌色
+          </t-button>
+        </template>
+      </t-popover>
+    </div>
+  </div>
+  <div class="popover-example">
+    <div class="popover-example__content">
+      <t-popover :visible="visibleList[3]" placement="top" theme="success" content="弹出气泡内容">
+        <template #triggerElement>
+          <t-button class="button-width--small" theme="primary" variant="outline" size="large" @click="handleClick(3)">
+            成功色
+          </t-button>
+        </template>
+      </t-popover>
+    </div>
+    <div class="popover-example__content">
+      <t-popover :visible="visibleList[4]" placement="top" theme="warning" content="弹出气泡内容">
+        <template #triggerElement>
+          <t-button class="button-width--small" theme="primary" variant="outline" size="large" @click="handleClick(4)">
+            警告色
+          </t-button>
+        </template>
+      </t-popover>
+    </div>
+    <div class="popover-example__content">
+      <t-popover :visible="visibleList[5]" placement="top" theme="error" content="弹出气泡内容">
+        <template #triggerElement>
+          <t-button class="button-width--small" theme="primary" variant="outline" size="large" @click="handleClick(5)">
+            错误色
+          </t-button>
+        </template>
+      </t-popover>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const visibleList = ref([false, false, false, false, false, false]);
+
+const handleClick = (number: number) => {
+  const newVisibleList = visibleList.value.map((item, index) => {
+    if (index === number) {
+      return !item;
+    }
+    return false;
+  });
+  visibleList.value = newVisibleList;
+};
+</script>
+
+<style lang="less" scoped>
+.popover-example {
+  display: flex;
+  padding: 0 16px;
+
+  &:not(:last-child) {
+    margin-bottom: 16px;
+  }
+
+  &__content {
+    flex: 1;
+
+    &:not(:last-child) {
+      margin-right: 16px;
+    }
+  }
+}
+
+.button-width--small {
+  width: 102px;
+}
+
+.mb-24 {
+  margin-bottom: 24px;
+}
+
+.mb-16 {
+  margin-bottom: 16px;
+}
+</style>
diff --git a/src/popover/demos/type.vue b/src/popover/demos/type.vue
new file mode 100644
index 00000000..5381774a
--- /dev/null
+++ b/src/popover/demos/type.vue
@@ -0,0 +1,107 @@
+<template>
+  <div class="popover-example">
+    <div class="popover-example__summary mb-16">带箭头的弹出气泡</div>
+    <div class="popover-example__content mb-24">
+      <t-popover
+        :visible="visibleList[0]"
+        placement="top"
+        theme="dark"
+        content="弹出气泡内容"
+        @visible-change="handleVisibleChange"
+      >
+        <template #triggerElement>
+          <t-button theme="primary" variant="outline" size="large" @click="handleClick(0)">带箭头</t-button>
+        </template>
+      </t-popover>
+    </div>
+  </div>
+  <div class="popover-example">
+    <div class="popover-example__summary mb-16">不带箭头的弹出气泡</div>
+    <div class="popover-example__content mb-24">
+      <t-popover :visible="visibleList[1]" placement="top" theme="dark" :show-arrow="false" content="弹出气泡内容">
+        <template #triggerElement>
+          <t-button theme="primary" variant="outline" size="large" @click="handleClick(1)">不带箭头</t-button>
+        </template>
+      </t-popover>
+    </div>
+  </div>
+  <div class="popover-example">
+    <div class="popover-example__summary mb-16">自定义内容弹出气泡</div>
+    <div class="custom popover-example__content mb-24">
+      <t-popover :visible="visibleList[2]" placement="top" theme="dark">
+        <template #triggerElement>
+          <t-button theme="primary" variant="outline" size="large" @click="handleClick(2)">自定义内容</t-button>
+        </template>
+        <template #content>
+          <div class="custom__list">
+            <div v-for="i in 3" :key="i" class="custom__item">选项{{ i }}</div>
+          </div>
+        </template>
+      </t-popover>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const visibleList = ref([false, false, false]);
+
+const handleClick = (number: number) => {
+  const newVisibleList = visibleList.value.map((item, index) => {
+    if (index === number) {
+      return !item;
+    }
+    return false;
+  });
+  visibleList.value = newVisibleList;
+};
+
+const handleVisibleChange = (visible: boolean) => {
+  console.log('visible is: ', visible);
+};
+</script>
+
+<style lang="less" scoped>
+.popover-example {
+  padding: 0 16px;
+
+  &__content {
+    display: flex;
+    justify-content: center;
+  }
+
+  &__summary {
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.6);
+    line-height: 22px;
+  }
+}
+
+.custom {
+  --td-popover-padding: 0;
+}
+.custom__list {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  color: #fff;
+}
+
+.custom__item {
+  width: 105px;
+  line-height: 24px;
+  text-align: center;
+  padding: 12px;
+
+  &:not(:last-child) {
+    border-bottom: 1px solid #fff;
+  }
+}
+.mb-24 {
+  margin-bottom: 24px;
+}
+
+.mb-16 {
+  margin-bottom: 16px;
+}
+</style>
diff --git a/src/popover/index.ts b/src/popover/index.ts
new file mode 100644
index 00000000..3e97de60
--- /dev/null
+++ b/src/popover/index.ts
@@ -0,0 +1,11 @@
+import Popover from './popover.vue';
+import { withInstall, WithInstallType } from '../shared';
+import { TdPopoverProps } from './type';
+
+import './style';
+
+export * from './type';
+export type PopoverProps = TdPopoverProps;
+
+const _Popover: WithInstallType<typeof Popover> = withInstall(Popover);
+export default _Popover;
diff --git a/src/popover/popover.en-US.md b/src/popover/popover.en-US.md
new file mode 100644
index 00000000..7f1646f5
--- /dev/null
+++ b/src/popover/popover.en-US.md
@@ -0,0 +1,21 @@
+:: BASE_DOC ::
+
+## API
+### Popover Props
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+content | String / Slot / Function | - | Typescript：`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+default | String / Slot / Function | - | Typescript：`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+placement | String | top | options：top/left/right/bottom/top-left/top-right/bottom-left/bottom-right/left-top/left-bottom/right-top/right-bottom | N
+showArrow | Boolean | true | \- | N
+theme | String | dark | options：dark/light/brand/success/warning/error | N
+triggerElement | String / Slot / Function | - | Typescript：`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+visible | Boolean | - | `v-model` and `v-model:visible` is supported | N
+onVisibleChange | Function |  | Typescript：`(visible: boolean) => void`<br/> | N
+
+### Popover Events
+
+name | params | description
+-- | -- | --
+visible-change | `(visible: boolean)` | \-
diff --git a/src/popover/popover.md b/src/popover/popover.md
new file mode 100644
index 00000000..c0a95091
--- /dev/null
+++ b/src/popover/popover.md
@@ -0,0 +1,21 @@
+:: BASE_DOC ::
+
+## API
+### Popover Props
+
+名称 | 类型 | 默认值 | 说明 | 必传
+-- | -- | -- | -- | --
+content | String / Slot / Function | - | 确认框内容。TS 类型：`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+default | String / Slot / Function | - | 触发元素，同 triggerElement。TS 类型：`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+placement | String | top | 浮层出现位置。可选项：top/left/right/bottom/top-left/top-right/bottom-left/bottom-right/left-top/left-bottom/right-top/right-bottom | N
+showArrow | Boolean | true | 是否显示浮层箭头 | N
+theme | String | dark | 弹出气泡主题。。可选项：dark/light/brand/success/warning/error | N
+triggerElement | String / Slot / Function | - | 触发元素。TS 类型：`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+visible | Boolean | - | 是否显示气泡确认框。支持语法糖 `v-model` 或 `v-model:visible` | N
+onVisibleChange | Function |  | TS 类型：`(visible: boolean) => void`<br/>确认框显示或隐藏时触发 | N
+
+### Popover Events
+
+名称 | 参数 | 描述
+-- | -- | --
+visible-change | `(visible: boolean)` | 确认框显示或隐藏时触发
diff --git a/src/popover/popover.vue b/src/popover/popover.vue
new file mode 100644
index 00000000..33c95415
--- /dev/null
+++ b/src/popover/popover.vue
@@ -0,0 +1,101 @@
+<template>
+  <div ref="referenceRef" :class="`${name}__wrapper`">
+    <t-node :content="triggerElementContent"></t-node>
+  </div>
+
+  <Transition :name="`${name}--animation`" appear @enter="updatePopper" @after-leave="destroyPopper">
+    <div v-show="currentVisible" ref="popoverRef" data-popper-placement :class="[`${name}`]">
+      <div :class="contentClasses">
+        <t-node :content="content"></t-node>
+        <div v-if="showArrow" :class="`${name}__arrow`" />
+      </div>
+    </div>
+  </Transition>
+</template>
+<script lang="ts">
+import { defineComponent, getCurrentInstance, computed, ref, toRefs, provide, watch } from 'vue';
+import { createPopper, Placement } from '@popperjs/core';
+import PopoverProps from './props';
+import { TdPopoverProps } from './type';
+import config from '../config';
+import { renderContent, renderTNode, TNode, useDefault } from '../shared';
+
+const { prefix } = config;
+const name = `${prefix}-popover`;
+
+export default defineComponent({
+  name,
+  components: { TNode },
+  props: PopoverProps,
+  emits: ['visible-change', 'update:visible', 'update:modelValue'],
+  setup(props, context) {
+    const [currentVisible, setVisible] = useDefault<TdPopoverProps['visible'], TdPopoverProps>(
+      props,
+      context.emit,
+      'visible',
+      'visible-change',
+    );
+
+    const internalInstance = getCurrentInstance();
+    const referenceRef = ref<HTMLElement>();
+    const popoverRef = ref<HTMLElement>();
+
+    const contentClasses = computed(() => [`${name}__content`, `${name}--${props.theme}`]);
+
+    /** popperjs instance */
+    let popper: ReturnType<typeof createPopper>;
+    const content = computed(() => renderTNode(internalInstance, 'content'));
+    const triggerElementContent = computed(() => renderContent(internalInstance, 'default', 'triggerElement'));
+
+    const getPopperPlacement = (placement: TdPopoverProps['placement']): Placement => {
+      return placement?.replace(/-(left|top)$/, '-start').replace(/-(right|bottom)$/, '-end') as Placement;
+    };
+
+    const getPopoverOptions = () => ({
+      placement: getPopperPlacement(props.placement),
+    });
+
+    const updatePopper = () => {
+      if (referenceRef.value && popoverRef.value) {
+        popper = createPopper(referenceRef.value, popoverRef.value, getPopoverOptions());
+      }
+      return null;
+    };
+
+    const destroyPopper = () => {
+      if (popper) {
+        popper?.destroy();
+        // @ts-ignore
+        popper = null;
+      }
+    };
+
+    watch(
+      () => currentVisible.value,
+      (val) => {
+        setVisible(val);
+      },
+    );
+
+    watch(
+      () => props.placement,
+      () => {
+        destroyPopper();
+        updatePopper();
+      },
+    );
+
+    return {
+      name,
+      currentVisible,
+      referenceRef,
+      popoverRef,
+      triggerElementContent,
+      content,
+      contentClasses,
+      updatePopper,
+      destroyPopper,
+    };
+  },
+});
+</script>
diff --git a/src/popover/props.ts b/src/popover/props.ts
new file mode 100644
index 00000000..9b99fef6
--- /dev/null
+++ b/src/popover/props.ts
@@ -0,0 +1,72 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件，请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TdPopoverProps } from './type';
+import { PropType } from 'vue';
+
+export default {
+  /** 确认框内容 */
+  content: {
+    type: [String, Function] as PropType<TdPopoverProps['content']>,
+  },
+  /** 触发元素，同 triggerElement */
+  default: {
+    type: [String, Function] as PropType<TdPopoverProps['default']>,
+  },
+  /** 浮层出现位置 */
+  placement: {
+    type: String as PropType<TdPopoverProps['placement']>,
+    default: 'top' as TdPopoverProps['placement'],
+    validator(val: TdPopoverProps['placement']): boolean {
+      if (!val) return true;
+      return [
+        'top',
+        'left',
+        'right',
+        'bottom',
+        'top-left',
+        'top-right',
+        'bottom-left',
+        'bottom-right',
+        'left-top',
+        'left-bottom',
+        'right-top',
+        'right-bottom',
+      ].includes(val);
+    },
+  },
+  /** 是否显示浮层箭头 */
+  showArrow: {
+    type: Boolean,
+    default: true,
+  },
+  /** 弹出气泡主题。 */
+  theme: {
+    type: String as PropType<TdPopoverProps['theme']>,
+    default: 'dark' as TdPopoverProps['theme'],
+    validator(val: TdPopoverProps['theme']): boolean {
+      if (!val) return true;
+      return ['dark', 'light', 'brand', 'success', 'warning', 'error'].includes(val);
+    },
+  },
+  /** 触发元素 */
+  triggerElement: {
+    type: [String, Function] as PropType<TdPopoverProps['triggerElement']>,
+  },
+  /** 是否显示气泡确认框 */
+  visible: {
+    type: Boolean,
+    default: undefined,
+  },
+  modelValue: {
+    type: Boolean,
+    default: undefined,
+  },
+  /** 是否显示气泡确认框，非受控属性 */
+  defaultVisible: Boolean,
+  /** 确认框显示或隐藏时触发 */
+  onVisibleChange: Function as PropType<TdPopoverProps['onVisibleChange']>,
+};
diff --git a/src/popover/style/css.js b/src/popover/style/css.js
new file mode 100644
index 00000000..6a9a4b13
--- /dev/null
+++ b/src/popover/style/css.js
@@ -0,0 +1,1 @@
+import './index.css';
diff --git a/src/popover/style/index.js b/src/popover/style/index.js
new file mode 100644
index 00000000..43c5780a
--- /dev/null
+++ b/src/popover/style/index.js
@@ -0,0 +1,1 @@
+import '../../_common/style/mobile/components/popover/_index.less';
diff --git a/src/popover/type.ts b/src/popover/type.ts
new file mode 100644
index 00000000..9769a8aa
--- /dev/null
+++ b/src/popover/type.ts
@@ -0,0 +1,65 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件，请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TNode } from '../common';
+
+export interface TdPopoverProps {
+  /**
+   * 确认框内容
+   */
+  content?: string | TNode;
+  /**
+   * 触发元素，同 triggerElement
+   */
+  default?: string | TNode;
+  /**
+   * 浮层出现位置
+   * @default top
+   */
+  placement?:
+    | 'top'
+    | 'left'
+    | 'right'
+    | 'bottom'
+    | 'top-left'
+    | 'top-right'
+    | 'bottom-left'
+    | 'bottom-right'
+    | 'left-top'
+    | 'left-bottom'
+    | 'right-top'
+    | 'right-bottom';
+  /**
+   * 是否显示浮层箭头
+   * @default true
+   */
+  showArrow?: boolean;
+  /**
+   * 弹出气泡主题。
+   * @default dark
+   */
+  theme?: 'dark' | 'light' | 'brand' | 'success' | 'warning' | 'error';
+  /**
+   * 触发元素
+   */
+  triggerElement?: string | TNode;
+  /**
+   * 是否显示气泡确认框
+   */
+  visible?: boolean;
+  /**
+   * 是否显示气泡确认框，非受控属性
+   */
+  defaultVisible?: boolean;
+  /**
+   * 是否显示气泡确认框
+   */
+  modelValue?: boolean;
+  /**
+   * 确认框显示或隐藏时触发
+   */
+  onVisibleChange?: (visible: boolean) => void;
+}
diff --git a/src/popup/popup.vue b/src/popup/popup.vue
index 617d2ae3..8f9b3a18 100644
--- a/src/popup/popup.vue
+++ b/src/popup/popup.vue
@@ -20,7 +20,7 @@ import popupProps from './props';
 import TOverlay from '../overlay';
 import config from '../config';
 import { TdPopupProps } from './type';
-import { useDefault, useEmitEvent, TNode, renderTNode } from '../shared';
+import { useDefault, TNode, renderTNode } from '../shared';
 import { getAttach } from '../shared/dom';
 
 const { prefix } = config;
@@ -35,7 +35,6 @@ export default defineComponent({
   emits: ['open', 'close', 'opened', 'closed', 'visible-change', 'update:visible', 'update:modelValue'],
   setup(props, context) {
     const currentInstance = getCurrentInstance();
-    const emitEvent = useEmitEvent(props, context.emit);
     const [currentVisible, setVisible] = useDefault<TdPopupProps['visible'], TdPopupProps>(
       props,
       context.emit,
@@ -89,7 +88,7 @@ export default defineComponent({
     );
 
     const handleCloseClick = (e: MouseEvent) => {
-      emitEvent('close', { e });
+      props.onClose?.({ e });
       setVisible(false);
     };
 
@@ -98,22 +97,22 @@ export default defineComponent({
       if (!props.closeOnOverlayClick) {
         return;
       }
-      emitEvent('close', { e });
+      props.onClose?.({ e });
       setVisible(false);
     };
 
     const afterLeave = () => {
       wrapperVisbile.value = false;
-      emitEvent('closed');
+      props.onClosed?.();
     };
-    const afterEnter = () => emitEvent('opened');
+    const afterEnter = () => props.onOpened?.();
     const to = computed(() => getAttach(props.attach ?? 'body'));
 
     watch(
       () => currentVisible.value,
       (val) => {
         if (val) {
-          emitEvent('open');
+          props.onOpen?.();
           setVisible(true);
         }
       },
diff --git a/src/popup/props.ts b/src/popup/props.ts
index 5a1ff0cb..41b757ec 100644
--- a/src/popup/props.ts
+++ b/src/popup/props.ts
@@ -32,7 +32,7 @@ export default {
   /** 浮层出现位置 */
   placement: {
     type: String as PropType<TdPopupProps['placement']>,
-    default: 'top' as TdPopupProps['placement'],
+    // default: 'top' as TdPopupProps['placement'],
     validator(val: TdPopupProps['placement']): boolean {
       if (!val) return true;
       return ['top', 'left', 'right', 'bottom', 'center'].includes(val);
diff --git a/src/popup/type.ts b/src/popup/type.ts
index b6dab241..9ff0e0cb 100644
--- a/src/popup/type.ts
+++ b/src/popup/type.ts
@@ -35,7 +35,7 @@ export interface TdPopupProps {
    * 浮层出现位置
    * @default top
    */
-  placement?: 'top' | 'left' | 'right' | 'bottom' | 'center';
+  placement?: 'top' | 'left' | 'right' | 'bottom' | 'center' | '';
   /**
    * 防止滚动穿透
    * @default true
diff --git a/src/pull-down-refresh/pull-down-refresh.vue b/src/pull-down-refresh/pull-down-refresh.vue
index daa5cbbf..ead8fcd8 100644
--- a/src/pull-down-refresh/pull-down-refresh.vue
+++ b/src/pull-down-refresh/pull-down-refresh.vue
@@ -27,7 +27,8 @@ import debounce from 'lodash/debounce';
 import isArray from 'lodash/isArray';
 
 import PullDownRefreshProps from './props';
-import { useEmitEvent, useVModel, convertUnit, reconvertUnit } from '../shared';
+import { useVModel, convertUnit, reconvertUnit } from '../shared';
+
 import config from '../config';
 import TLoading from '../loading';
 import { useTouch, isReachTop, easeDistance } from './useTouch';
@@ -41,8 +42,7 @@ export default defineComponent({
   components: { TLoading },
   props: PullDownRefreshProps,
   emits: ['refresh', 'timeout', 'scrolltolower', 'update:value', 'update:modelValue'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
     let timer: any = null;
 
     // 滑动距离
@@ -137,10 +137,10 @@ export default defineComponent({
       if (status.value === 'loosing') {
         distance.value = loadingBarHeight.value;
         setLoading(true);
-        emitEvent('refresh');
+        props.onRefresh?.();
         timer = setTimeout(() => {
           if (loading.value) {
-            emitEvent('timeout');
+            props.onTimeout?.();
             setLoading(false);
           }
         }, props.refreshTimeout);
@@ -155,7 +155,7 @@ export default defineComponent({
         const { clientHeight, scrollHeight } = document.documentElement; // 可视区域/屏幕高度， 页面高度
         const distance = 20; // 距离视窗 20 时，开始触发
         if (scrollTop + clientHeight >= scrollHeight - distance) {
-          emitEvent('scrolltolower');
+          props.onScrolltolower?.();
         }
       },
       300,
diff --git a/src/search/search.vue b/src/search/search.vue
index 4d57fb34..24c038b7 100644
--- a/src/search/search.vue
+++ b/src/search/search.vue
@@ -37,7 +37,7 @@ import { ref, computed, defineComponent, toRefs, getCurrentInstance, h, nextTick
 import isFunction from 'lodash/isFunction';
 import { useFocus } from '@vueuse/core';
 import config from '../config';
-import { renderTNode, TNode, useEmitEvent } from '../shared';
+import { renderTNode, TNode } from '../shared';
 import searchProps from './props';
 import { useDefault } from '../shared/useDefault';
 
@@ -50,7 +50,6 @@ export default defineComponent({
   props: searchProps,
   emits: ['update:value', 'update:modelValue', 'action-click', 'focus', 'blur', 'change', 'clear', 'submit'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const searchInput = ref();
     const { focused } = useFocus(searchInput, { initialValue: props.focus });
     const [searchValue] = useDefault(props, context.emit, 'value', 'change');
@@ -102,15 +101,15 @@ export default defineComponent({
     const handleClear = (e: MouseEvent) => {
       searchValue.value = '';
       focused.value = true;
-      emitEvent('clear', { e });
+      props.onClear?.({ e });
     };
 
     const handleFocus = (e: FocusEvent) => {
-      emitEvent('focus', { value: searchValue.value, e });
+      props.onFocus?.({ value: searchValue.value, e });
     };
 
     const handleBlur = (e: FocusEvent) => {
-      emitEvent('blur', { value: searchValue.value, e });
+      props.onBlur?.({ value: searchValue.value, e });
     };
 
     const handleCompositionend = (e: InputEvent | CompositionEvent) => {
@@ -118,14 +117,14 @@ export default defineComponent({
     };
 
     const onActionClick = (e: MouseEvent) => {
-      emitEvent('action-click', { e: MouseEvent });
+      props.onActionClick?.({ e });
     };
 
     const handleSearch = (e: any) => {
       // 如果按的是 enter 键, 13是 enter
       if (e.keyCode === 13) {
         e.preventDefault(); // 禁止默认（换行）事件
-        emitEvent('submit', { value: searchValue.value, e });
+        props.onSubmit?.({ value: searchValue.value, e });
       }
     };
 
diff --git a/src/shared/index.ts b/src/shared/index.ts
index 94251f85..0131e94c 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -9,7 +9,6 @@ export { default as TNode } from './render-tnode';
 export * from './useToggle';
 export * from './useCountDown';
 export * from './useDefault';
-export * from './useEmitEvent';
 export * from './useChildSlots';
 export * from './useVModel';
 export * from './useTouch';
diff --git a/src/shared/useEmitEvent/index.ts b/src/shared/useEmitEvent/index.ts
deleted file mode 100644
index 8ee8a2a7..00000000
--- a/src/shared/useEmitEvent/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import camelCase from 'lodash/camelCase';
-
-export type EmitEventName = { event: string; method: string } | string;
-
-/**
- * 组件对外传递事件
- * @param props 组件props
- * @param emit 事件名(注意使用中划线)
- * @param args 事件参数
- * @returns {emitEvent}
- * @example useEmitEvent<IProps>(props, emit);
- */
-export function useEmitEvent<P extends Record<string, any>>(props: P, emit: (...args: any[]) => void) {
-  return function emitEvent<T extends any[] = any[]>(eventName: string, ...args: T) {
-    let emitEventMethodName = '';
-    if (typeof eventName === 'string') {
-      emitEventMethodName = camelCase(`on-${eventName}`);
-    }
-
-    if (typeof props[emitEventMethodName] === 'function') {
-      props[emitEventMethodName](...args);
-    } else {
-      emit(eventName, ...args);
-    }
-  };
-}
diff --git a/src/side-bar/side-bar.vue b/src/side-bar/side-bar.vue
index c3e7b19f..75addb8c 100644
--- a/src/side-bar/side-bar.vue
+++ b/src/side-bar/side-bar.vue
@@ -10,7 +10,7 @@ import { defineComponent, ref, Ref, ComponentInternalInstance, provide, toRefs }
 import config from '../config';
 import SideBarProps from './props';
 import { TdSideBarProps } from './type';
-import { useEmitEvent, useDefault } from '../shared';
+import { useDefault } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-side-bar`;
@@ -20,7 +20,6 @@ export default defineComponent({
   props: SideBarProps,
   emits: ['update:value', 'update:modelValue', 'change'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const [currentValue, setCurrentValue] = useDefault<TdSideBarProps['value'], TdSideBarProps>(
       props,
       context.emit,
@@ -38,9 +37,9 @@ export default defineComponent({
       children.value = children.value.filter((item) => item !== child);
     };
 
-    const onClickItem = (cur: TdSideBarProps['value'], label: string) => {
+    const onClickItem = (cur: string | number, label: string) => {
       setCurrentValue(cur);
-      emitEvent('click', cur, label);
+      props.onClick?.(cur, label);
     };
 
     provide('sideBarProvide', {
diff --git a/src/stepper/props.ts b/src/stepper/props.ts
index 3af21942..4d2d3887 100644
--- a/src/stepper/props.ts
+++ b/src/stepper/props.ts
@@ -67,6 +67,8 @@ export default {
   onBlur: Function as PropType<TdStepperProps['onBlur']>,
   /** 数值发生变更时触发 */
   onChange: Function as PropType<TdStepperProps['onChange']>,
+  /** 输入框聚焦时触发 */
+  onFocus: Function as PropType<TdStepperProps['onFocus']>,
   /** 数值超出限制时触发 */
   onOverlimit: Function as PropType<TdStepperProps['onOverlimit']>,
 };
diff --git a/src/stepper/stepper.en-US.md b/src/stepper/stepper.en-US.md
index 408c6c2a..20fd3427 100644
--- a/src/stepper/stepper.en-US.md
+++ b/src/stepper/stepper.en-US.md
@@ -17,6 +17,7 @@ value | String / Number | 0 | `v-model` and `v-model:value` is supported | N
 defaultValue | String / Number | 0 | uncontrolled property | N
 onBlur | Function |  | Typescript：`(value: string \| number) => void`<br/> | N
 onChange | Function |  | Typescript：`(value: string \| number) => void`<br/> | N
+onFocus | Function |  | Typescript：`(value: string \| number) => void`<br/> | N
 onOverlimit | Function |  | Typescript：`(type: 'minus' \| 'plus') => void`<br/> | N
 
 ### Stepper Events
@@ -25,4 +26,5 @@ name | params | description
 -- | -- | --
 blur | `(value: string \| number)` | \-
 change | `(value: string \| number)` | \-
+focus | `(value: string \| number)` | \-
 overlimit | `(type: 'minus' \| 'plus')` | \-
diff --git a/src/stepper/stepper.md b/src/stepper/stepper.md
index 4184b724..9f9163c9 100644
--- a/src/stepper/stepper.md
+++ b/src/stepper/stepper.md
@@ -17,6 +17,7 @@ value | String / Number | 0 | 值。支持语法糖 `v-model` 或 `v-model:value
 defaultValue | String / Number | 0 | 值。非受控属性 | N
 onBlur | Function |  | TS 类型：`(value: string \| number) => void`<br/>输入框失去焦点时触发 | N
 onChange | Function |  | TS 类型：`(value: string \| number) => void`<br/>数值发生变更时触发 | N
+onFocus | Function |  | TS 类型：`(value: string \| number) => void`<br/>输入框聚焦时触发 | N
 onOverlimit | Function |  | TS 类型：`(type: 'minus' \| 'plus') => void`<br/>数值超出限制时触发 | N
 
 ### Stepper Events
@@ -25,4 +26,5 @@ onOverlimit | Function |  | TS 类型：`(type: 'minus' \| 'plus') => void`<br/>
 -- | -- | --
 blur | `(value: string \| number)` | 输入框失去焦点时触发
 change | `(value: string \| number)` | 数值发生变更时触发
+focus | `(value: string \| number)` | 输入框聚焦时触发
 overlimit | `(type: 'minus' \| 'plus')` | 数值超出限制时触发
diff --git a/src/stepper/stepper.vue b/src/stepper/stepper.vue
index 2ea1b0f6..1c09eab7 100644
--- a/src/stepper/stepper.vue
+++ b/src/stepper/stepper.vue
@@ -47,7 +47,7 @@ import { toRefs, computed, defineComponent } from 'vue';
 import { AddIcon, RemoveIcon } from 'tdesign-icons-vue-next';
 import config from '../config';
 import StepperProps from './props';
-import { useDefault, useEmitEvent } from '../shared';
+import { useDefault } from '../shared';
 import { TdStepperProps } from './type';
 import { useFormDisabled } from '../form/hooks';
 
@@ -63,7 +63,6 @@ export default defineComponent({
   emits: ['update:value', 'update:modelValue', 'blur', 'change', 'overlimit'],
   setup(props, context) {
     const [stepperValue] = useDefault<number, TdStepperProps>(props, context.emit, 'value', 'change');
-    const emitEvent = useEmitEvent(props, context.emit);
     const disabled = useFormDisabled();
     const { min, max, step, inputWidth } = toRefs(props);
     const inputStyle = computed(() => (inputWidth ? { width: `${inputWidth.value}px` } : ''));
@@ -88,7 +87,7 @@ export default defineComponent({
 
     const plusValue = () => {
       if (isDisabled('plus')) {
-        emitEvent('overlimit', 'plus');
+        props.onOverlimit?.('plus');
         return;
       }
       updateValue(Number(stepperValue.value) + step.value);
@@ -96,7 +95,7 @@ export default defineComponent({
 
     const minusValue = () => {
       if (isDisabled('minus')) {
-        emitEvent('overlimit', 'minus');
+        props.onOverlimit?.('minus');
         return;
       }
       updateValue(Number(stepperValue.value) - step.value);
@@ -113,11 +112,11 @@ export default defineComponent({
     };
 
     const handleFocus = () => {
-      emitEvent('focus', stepperValue.value);
+      props.onFocus?.(stepperValue.value);
     };
 
     const handleBlur = () => {
-      emitEvent('blur', stepperValue.value);
+      props.onBlur?.(stepperValue.value);
     };
 
     return {
diff --git a/src/stepper/type.ts b/src/stepper/type.ts
index 5af9d9ba..d4d8762d 100644
--- a/src/stepper/type.ts
+++ b/src/stepper/type.ts
@@ -69,6 +69,10 @@ export interface TdStepperProps {
    * 数值发生变更时触发
    */
   onChange?: (value: string | number) => void;
+  /**
+   * 输入框聚焦时触发
+   */
+  onFocus?: (value: string | number) => void;
   /**
    * 数值超出限制时触发
    */
diff --git a/src/sticky/sticky.vue b/src/sticky/sticky.vue
index 19c803c8..7fd55571 100644
--- a/src/sticky/sticky.vue
+++ b/src/sticky/sticky.vue
@@ -11,7 +11,7 @@ import { computed, getCurrentInstance, defineComponent, ref, watch } from 'vue';
 import { useElementBounding, templateRef } from '@vueuse/core';
 import props from './props';
 import config from '../config';
-import { renderContent, useEmitEvent, TNode } from '../shared';
+import { renderContent, TNode } from '../shared';
 
 const name = `${config.prefix}-sticky`;
 
@@ -19,10 +19,9 @@ export default defineComponent({
   name,
   components: { TNode },
   props,
-  setup(props, context) {
+  setup(props) {
     const boxClasses = name;
     const stickyContent = computed(() => renderContent(getCurrentInstance(), 'default', ''));
-    const emitEvent = useEmitEvent(props, context.emit);
 
     // box 用于占位和记录边界
     // content 用于实际定位
@@ -66,7 +65,7 @@ export default defineComponent({
         styleStr += `position:fixed;top:${offsetTop}px;`;
         isFixed = true;
       }
-      emitEvent('scroll', { scrollTop: contentTop.value, isFixed });
+      props.onScroll?.({ scrollTop: contentTop.value, isFixed });
       return styleStr;
     });
 
diff --git a/src/swipe-cell/swipe-cell.vue b/src/swipe-cell/swipe-cell.vue
index f026edeb..d7de7a6d 100644
--- a/src/swipe-cell/swipe-cell.vue
+++ b/src/swipe-cell/swipe-cell.vue
@@ -91,7 +91,7 @@ import { useSwipe } from './useSwipe';
 import props from './props';
 import config from '../config';
 import { SwipeActionItem } from './type';
-import { renderContent, renderTNode, TNode, useEmitEvent, useClickAway } from '../shared';
+import { renderContent, renderTNode, TNode, useClickAway } from '../shared';
 import { useSureConfirm } from './useSureConfirm';
 
 const { prefix } = config;
@@ -113,7 +113,6 @@ export default defineComponent({
   props,
   emits: ['click', 'change'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const internalInstance = getCurrentInstance();
     const swipeContent = computed(() => renderContent(internalInstance, 'default', 'content'));
     const swipeLeftMenu = computed(() =>
@@ -356,12 +355,12 @@ export default defineComponent({
       if (direction === 'toLeft') {
         initData.pos = -initData.rightWidth;
         if (initData.rightWidth && !isOpen) {
-          emitEvent('change', 'right');
+          props.onChange?.('right');
         }
       } else {
         initData.pos = initData.leftWidth;
         if (initData.leftWidth && !isOpen) {
-          emitEvent('change', 'left');
+          props.onChange?.('left');
         }
       }
     };
@@ -373,7 +372,7 @@ export default defineComponent({
       sureMarginRight.value = 0;
       initData.status = 'close';
       initData.pos = 0;
-      emitEvent('change', undefined);
+      props.onChange?.(undefined);
     };
     const handleClickBtn = ({ action, source }: { action: SwipeActionItem; source: String }) => {
       const clickFn = () => {
@@ -384,7 +383,7 @@ export default defineComponent({
           action.onClick();
           return;
         }
-        emitEvent('click', { action, source });
+        props.onClick?.({ action, source });
       };
       if (action.sure) {
         showSure(action.sure, clickFn);
diff --git a/src/swiper/swiper.vue b/src/swiper/swiper.vue
index bdc80fd4..a7b6f3fe 100644
--- a/src/swiper/swiper.vue
+++ b/src/swiper/swiper.vue
@@ -58,7 +58,7 @@ import { isNumber } from 'lodash';
 
 import config from '../config';
 import SwiperProps from './props';
-import { renderTNode, TNode, useEmitEvent, useVModel } from '../shared';
+import { renderTNode, TNode, useVModel } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-swiper`;
@@ -81,7 +81,6 @@ const items = ref<any>([]);
 const props = defineProps(SwiperProps);
 const emit = defineEmits(['change', 'update:current', 'update:modelValue']);
 
-const emitEvent = useEmitEvent(props, emit);
 const [current, setCurrent] = useVModel(
   ref(props.current),
   ref(props.modelValue),
@@ -112,7 +111,7 @@ const enableNavigation = computed(() => {
 let autoplayTimer: any = null;
 
 const onItemClick = () => {
-  emitEvent('click', current.value);
+  props.onClick?.(current.value ?? 0);
 };
 
 const move = (step: number) => {
diff --git a/src/table/base-table.vue b/src/table/base-table.vue
index 3eec3bde..9ce3d50e 100644
--- a/src/table/base-table.vue
+++ b/src/table/base-table.vue
@@ -88,7 +88,7 @@ import TdBaseTableProps from './base-table-props';
 import config from '../config';
 import useClassName from './hooks/useClassName';
 import useStyle, { formatCSSUnit } from './hooks/useStyle';
-import { renderTNode, TNode, useEmitEvent } from '../shared';
+import { renderTNode, TNode } from '../shared';
 import { TableRowData } from './type';
 import TLoading from '../loading';
 
@@ -100,9 +100,7 @@ export default defineComponent({
   components: { TNode },
   props: TdBaseTableProps,
   emits: ['cell-click', 'row-click', 'scroll'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
-
+  setup(props) {
     const { classPrefix, tableLayoutClasses, tableHeaderClasses, tableBaseClass, tdAlignClasses, tdEllipsisClass } =
       useClassName();
 
@@ -124,16 +122,14 @@ export default defineComponent({
     const ellipsisClasses = computed(() => [`${classPrefix}-table__ellipsis`, `${classPrefix}-text-ellipsis`]);
 
     const handleRowClick = (row: TableRowData, rowIndex: number, e: MouseEvent) => {
-      const p = { row, index: rowIndex, e };
-      emitEvent('rowClick', { context: p });
+      props.onRowClick?.({ row, index: rowIndex, e });
     };
 
     const handleCellClick = (row: TableRowData, col: any, rowIndex: number, colIndex: number, e: MouseEvent) => {
-      const p = { row, col, rowIndex, colIndex, e };
       if (col.stopPropagation) {
         e.stopPropagation();
       }
-      emitEvent('cellClick', { context: p });
+      props.onCellClick?.({ row, col, rowIndex, colIndex, e });
     };
 
     const dynamicBaseTableClasses = computed(() => [tableClasses.value]);
@@ -148,7 +144,7 @@ export default defineComponent({
     );
 
     const onInnerVirtualScroll = (e: WheelEvent) => {
-      emitEvent('scroll', { params: e });
+      props.onScroll?.({ params: e });
     };
 
     return {
diff --git a/src/tabs/tabs.vue b/src/tabs/tabs.vue
index f21dd738..0042c268 100644
--- a/src/tabs/tabs.vue
+++ b/src/tabs/tabs.vue
@@ -62,7 +62,7 @@ import {
 import config from '../config';
 import TabsProps from './props';
 import TabNavItem from './tab-nav-item.vue';
-import { useVModel, useEmitEvent } from '../shared';
+import { useVModel } from '../shared';
 import CLASSNAMES from '../shared/constants';
 import TSticky from '../sticky';
 
@@ -75,7 +75,6 @@ export default defineComponent({
   props: TabsProps,
   emits: ['update:value', 'update:modelValue'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const placement = ref('top');
     const theme = computed(() => props.theme);
     const spaceEvenly = computed(() => props.spaceEvenly);
@@ -177,7 +176,7 @@ export default defineComponent({
         return false;
       }
       setCurrentValue(value, typeof label === 'function' ? label() : label);
-      emitEvent('click', value, typeof label === 'function' ? label() : label);
+      props.onClick?.(value, typeof label === 'function' ? label() : label);
       nextTick(() => {
         moveToActiveTab();
       });
@@ -186,7 +185,7 @@ export default defineComponent({
     const handlerScroll = (context: { scrollTop: number; isFixed: boolean }) => {
       const { scrollTop, isFixed } = context;
       if (props.stickyProps) {
-        emitEvent('scroll', scrollTop, isFixed);
+        props.onScroll?.(scrollTop, isFixed);
       }
     };
 
diff --git a/src/tag/check-tag-props.ts b/src/tag/check-tag-props.ts
index db6bd245..6b6e0a5c 100644
--- a/src/tag/check-tag-props.ts
+++ b/src/tag/check-tag-props.ts
@@ -19,8 +19,6 @@ export default {
   },
   /** 标签选中的状态，默认风格（theme=default）才有选中态，非受控属性 */
   defaultChecked: Boolean,
-  /** 已废弃。标签是否可关闭 */
-  closable: Boolean,
   /** 组件子元素；传入数组时：[选中内容，非选中内容] */
   content: {
     type: [String, Number, Array, Function] as PropType<TdCheckTagProps['content']>,
diff --git a/src/tag/check-tag.vue b/src/tag/check-tag.vue
index ce31728c..8ceaaefd 100644
--- a/src/tag/check-tag.vue
+++ b/src/tag/check-tag.vue
@@ -9,18 +9,14 @@
       </template>
       <t-node v-else :content="tagContent"></t-node>
     </span>
-    <span v-if="closable && !disabled" :class="`${baseClass}__icon-close`" @click="onClickClose">
-      <close-icon />
-    </span>
   </span>
 </template>
 
 <script lang="ts">
-import { CloseIcon } from 'tdesign-icons-vue-next';
 import { defineComponent, computed, toRefs, getCurrentInstance } from 'vue';
 import config from '../config';
 import CheckTagProps from './check-tag-props';
-import { useEmitEvent, renderContent, renderTNode, TNode, useVModel } from '../shared';
+import { renderContent, renderTNode, TNode, useVModel } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-check-tag`;
@@ -28,13 +24,11 @@ const name = `${prefix}-check-tag`;
 const CheckTag = defineComponent({
   name,
   components: {
-    CloseIcon,
     TNode,
   },
   props: CheckTagProps,
   emits: ['change', 'click', 'update:checked', 'update:modelValue'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
     const internalInstance = getCurrentInstance();
     const tagContent = computed(() => renderContent(internalInstance, 'default', 'content'));
     const iconContent = computed(() => renderTNode(internalInstance, 'icon'));
@@ -64,21 +58,14 @@ const CheckTag = defineComponent({
       `${baseClass}--${props.size}`,
       `${baseClass}--${props.variant}`,
       {
-        [`${prefix}-is-closable ${baseClass}--closable`]: props.closable,
         [`${prefix}-is-disabled ${baseClass}--disabled`]: props.disabled,
         [`${prefix}-is-checked ${baseClass}--checked`]: !props.disabled && innerChecked.value,
       },
     ]);
 
-    const onClickClose = (e: MouseEvent): void => {
-      if (!props.disabled) {
-        emitEvent('close', { e });
-      }
-    };
-
     const handleClick = (e: MouseEvent) => {
       if (!props.disabled) {
-        emitEvent('click', { e });
+        props.onClick?.({ e });
         setInnerChecked(!innerChecked.value);
       }
     };
@@ -87,7 +74,6 @@ const CheckTag = defineComponent({
       contentIsArray,
       baseClass,
       classes,
-      onClickClose,
       handleClick,
       iconContent,
       tagContent,
diff --git a/src/tag/tag.vue b/src/tag/tag.vue
index 06801740..64ae1b18 100644
--- a/src/tag/tag.vue
+++ b/src/tag/tag.vue
@@ -17,7 +17,7 @@ import { CloseIcon } from 'tdesign-icons-vue-next';
 import { defineComponent, computed, getCurrentInstance } from 'vue';
 import config from '../config';
 import TagProps from './props';
-import { useEmitEvent, renderContent, renderTNode, TNode } from '../shared';
+import { renderContent, renderTNode, TNode } from '../shared';
 
 const { prefix } = config;
 const name = `${prefix}-tag`;
@@ -30,8 +30,7 @@ const Tag = defineComponent({
   },
   props: TagProps,
   emits: ['close', 'click'],
-  setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
+  setup(props) {
     const internalInstance = getCurrentInstance();
     const tagContent = computed(() => renderContent(internalInstance, 'default', 'content'));
     const iconContent = computed(() => renderTNode(internalInstance, 'icon'));
@@ -57,13 +56,16 @@ const Tag = defineComponent({
 
     const onClickClose = (e: MouseEvent): void => {
       if (!props.disabled) {
-        emitEvent('close', { e });
+        console.log(1);
+        console.log(props.onClose);
+
+        props.onClose?.({ e });
       }
     };
 
     const handleClick = (e: MouseEvent): void => {
       if (!props.disabled) {
-        emitEvent('click', { e });
+        props.onClick?.({ e });
       }
     };
 
diff --git a/src/tag/type.ts b/src/tag/type.ts
index 184a61c9..16c78a29 100644
--- a/src/tag/type.ts
+++ b/src/tag/type.ts
@@ -76,12 +76,6 @@ export interface TdCheckTagProps {
    * 标签选中的状态，默认风格（theme=default）才有选中态
    */
   modelValue?: boolean;
-  /**
-   * 标签是否可关闭
-   * @default false
-   * @deprecated
-   */
-  closable?: boolean;
   /**
    * 组件子元素；传入数组时：[选中内容，非选中内容]
    */
diff --git a/src/textarea/demos/maxcharacter.vue b/src/textarea/demos/maxcharacter.vue
index 94491045..d0d49c48 100644
--- a/src/textarea/demos/maxcharacter.vue
+++ b/src/textarea/demos/maxcharacter.vue
@@ -1,11 +1,8 @@
 <template>
-  <t-textarea
-    name="标签文字"
-    label="标签文字"
-    placeholder="请输入文字"
-    :maxcharacter="500"
-    indicator
-    :allow-input-over-max="true"
-  />
+  <t-textarea v-model="value" name="标签文字" label="标签文字" placeholder="请输入文字" :maxcharacter="500" indicator />
 </template>
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const value = ref('');
+</script>
diff --git a/src/textarea/demos/maxlength.vue b/src/textarea/demos/maxlength.vue
index 94ec5f0c..7b610c57 100644
--- a/src/textarea/demos/maxlength.vue
+++ b/src/textarea/demos/maxlength.vue
@@ -1,11 +1,8 @@
 <template>
-  <t-textarea
-    name="标签文字"
-    label="标签文字"
-    placeholder="请输入文字"
-    :maxlength="500"
-    indicator
-    :allow-input-over-max="true"
-  />
+  <t-textarea v-model="value" name="标签文字" label="标签文字" placeholder="请输入文字" :maxlength="500" indicator />
 </template>
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const value = ref('');
+</script>
diff --git a/src/textarea/textarea.vue b/src/textarea/textarea.vue
index 044205d2..cdcbb70a 100644
--- a/src/textarea/textarea.vue
+++ b/src/textarea/textarea.vue
@@ -27,7 +27,7 @@
 
 <script lang="ts">
 import { computed, ref, onMounted, defineComponent, getCurrentInstance, toRefs, nextTick, watch } from 'vue';
-import { useEmitEvent, renderTNode, TNode, getCharacterLength, useVModel } from '../shared';
+import { renderTNode, TNode, getCharacterLength, useVModel } from '../shared';
 import config from '../config';
 import TextareaProps from './props';
 import { TdTextareaProps, TextareaValue } from './type';
@@ -45,7 +45,6 @@ export default defineComponent({
   props: TextareaProps,
   emits: ['update:value', 'update:modelValue', 'focus', 'blur', 'change'],
   setup(props, context) {
-    const emitEvent = useEmitEvent(props, context.emit);
     const disabled = useFormDisabled();
     const textareaRef = ref<null | HTMLElement>(null);
     const textareaStyle = ref();
@@ -94,47 +93,47 @@ export default defineComponent({
 
     const handleInput = (e: any) => {
       if (e.isComposing || e.inputType === 'insertCompositionText') return;
-      textareaValueChangeHandle(e);
+      textareaValueChangeHandle();
     };
 
-    const textareaValueChangeHandle = (e: InputEvent) => {
-      const { target } = e;
-      const { value } = target as HTMLInputElement;
+    const textareaValueChangeHandle = () => {
+      const textarea = textareaRef.value as HTMLInputElement;
       if (
         !props.allowInputOverMax &&
         props.maxcharacter &&
         props.maxcharacter > 0 &&
         !Number.isNaN(props.maxcharacter)
       ) {
-        const { length = 0, characters = '' } = getCharacterLength(value, props.maxcharacter) as {
+        const { length = 0, characters = '' } = getCharacterLength(textarea.value, props.maxcharacter) as {
           length: number;
           characters: string;
         };
         setInnerValue(characters);
         textareaLength.value = length;
       } else {
-        setInnerValue(value);
-        textareaLength.value = String(innerValue.value).length;
+        setInnerValue(textarea.value);
+        textareaLength.value = String(textarea.value).length;
       }
       nextTick(() => setInputValue(innerValue.value));
       adjustTextareaHeight();
     };
 
     const handleCompositionend = (e: InputEvent | CompositionEvent) => {
-      textareaValueChangeHandle(e as InputEvent);
+      textareaValueChangeHandle();
     };
 
     const handleFocus = (e: FocusEvent) => {
-      emitEvent('focus', innerValue.value, { e });
+      props.onFocus?.(innerValue.value, { e });
     };
     const handleBlur = (e: FocusEvent) => {
-      emitEvent('blur', innerValue.value, { e });
+      props.onBlur?.(innerValue.value, { e });
     };
 
     onMounted(() => {
       if (props.autofocus) {
         textareaRef.value?.focus();
       }
+      textareaValueChangeHandle();
       adjustTextareaHeight();
     });
 
diff --git a/src/upload/upload.vue b/src/upload/upload.vue
index 9b2905ea..592d5547 100644
--- a/src/upload/upload.vue
+++ b/src/upload/upload.vue
@@ -57,7 +57,7 @@ import isFunction from 'lodash/isFunction';
 import TImage from '../image';
 import TImageViewer from '../image-viewer';
 import xhr from '../_common/js/upload/xhr';
-import { useDefault, useEmitEvent, renderTNode, TNode } from '../shared';
+import { useDefault, renderTNode, TNode } from '../shared';
 import { TdUploadProps, UploadFile, RequestMethodResponse, SizeLimitObj } from './type';
 import { SuccessContext, InnerProgressContext } from './interface';
 import UploadProps from './props';
@@ -93,12 +93,11 @@ export default defineComponent({
     'select-change',
     'validate',
   ],
-  setup(props, context) {
+  setup(props, { emit }) {
     const disabled = useFormDisabled();
-    const emitEvent = useEmitEvent(props, context.emit);
     const [innerFiles, setInnerFiles] = useDefault<TdUploadProps['files'], TdUploadProps>(
       props,
-      context.emit,
+      emit,
       'files',
       'change',
     );
@@ -129,7 +128,7 @@ export default defineComponent({
 
     const handlePreview = (e: MouseEvent, file: UploadFile) => {
       showViewer.value = true;
-      emitEvent('preview', {
+      emit('preview', {
         e,
         file,
       });
@@ -144,7 +143,7 @@ export default defineComponent({
       if (disabled.value || !input || !input.files) return;
 
       const formatFiles = formatFileToUploadFile(input.files);
-      emitEvent('select-change', [...formatFiles]);
+      emit?.('select-change', [...formatFiles], { currentSelectedFiles: uploadedFiles.value });
       uploadFiles(formatFiles);
       input.value = '';
     };
@@ -176,7 +175,7 @@ export default defineComponent({
         if (props.sizeLimit) {
           const isOverSizeLimit = handleSizeLimit(file.size || 0);
           if (isOverSizeLimit) {
-            emitEvent('validate', { type: 'FILE_OVER_SIZE_LIMIT', files: [file] });
+            props.onValidate?.({ type: 'FILE_OVER_SIZE_LIMIT', files: [file] });
           }
           resolve(!handleSizeLimit(file.size || 0));
         }
@@ -231,7 +230,7 @@ export default defineComponent({
           } else {
             const isDuplicated = toUploadFiles.value.some((file) => file.name === uploadFile.name);
             if (isDuplicated) {
-              emitEvent('validate', {
+              props.onValidate?.({
                 type: 'FILTER_FILE_SAME_NAME',
                 files: [uploadFile],
               });
@@ -273,14 +272,14 @@ export default defineComponent({
         file,
         type,
       };
-      emitEvent('progress', progressCtx);
+      props.onProgress?.(progressCtx);
     };
 
     const handleRemove = (e: MouseEvent, file: UploadFile, index: number) => {
       const files = uploadedFiles.value.concat();
       files.splice(index, 1);
       setInnerFiles(files, { e, trigger: 'remove', index, file });
-      emitEvent('remove', { e, index, file });
+      emit('remove', { e, index, file });
       images.value.splice(index, 1);
     };
 
@@ -381,7 +380,7 @@ export default defineComponent({
       const newFile = { ...file, response: res };
       const files = uploadedFiles.value.concat(newFile as UploadFile);
       setInnerFiles(files, { e: event, response: res, trigger: 'upload-success' });
-      emitEvent('success', {
+      emit('success', {
         file,
         fileList: files,
         e: event,
@@ -410,7 +409,7 @@ export default defineComponent({
         const files = uploadedFiles.value.concat(file);
         setInnerFiles(files, { e: event, response: res, trigger: 'upload-fail' });
       }
-      emitEvent('fail', { e: event, file });
+      props.onFail?.({ e: event, file });
     };
 
     return {
@@ -427,7 +426,6 @@ export default defineComponent({
       uploadedFiles,
       defaultContent,
       addContent,
-      emitEvent,
       setInnerFiles,
       triggerUpload,
       handleChange,


Test patch:
diff --git a/src/back-top/__test__/index.test.jsx b/src/back-top/__test__/index.test.jsx
index 602945df..582a0214 100644
--- a/src/back-top/__test__/index.test.jsx
+++ b/src/back-top/__test__/index.test.jsx
@@ -81,9 +81,10 @@ describe('BackTop', () => {
 
   describe('events', () => {
     it(': to-top', async () => {
-      const wrapper = mount(BackTop);
+      const onToTop = vi.fn();
+      const wrapper = mount(<BackTop onToTop={onToTop} />);
       wrapper.find('.t-back-top').trigger('click');
-      expect(wrapper.emitted('to-top')).toHaveLength(1);
+      expect(onToTop).toBeCalledTimes(1);
     });
   });
 });
diff --git a/src/drawer/__test__/index.test.jsx b/src/drawer/__test__/index.test.jsx
index bc7387de..4ea6f006 100644
--- a/src/drawer/__test__/index.test.jsx
+++ b/src/drawer/__test__/index.test.jsx
@@ -37,10 +37,7 @@ describe('drawer', () => {
     });
 
     it(': closeOnOverlayClick', async () => {
-      let triggerOrigin = {};
-      const onClose = ({ trigger }) => {
-        triggerOrigin = trigger;
-      };
+      const onClose = vi.fn();
       const closeOverlayClick = ref(true);
 
       const visible = ref(true);
@@ -54,7 +51,7 @@ describe('drawer', () => {
       expect($overlay.exists()).toBeTruthy();
 
       $overlay.find('.t-overlay').trigger('click');
-      expect(triggerOrigin).toBe('overlay');
+      expect(onClose).toBeCalledWith('overlay');
     });
 
     it(': items', async () => {
diff --git a/src/image/__test__/__snapshots__/demo.test.jsx.snap b/src/image/__test__/__snapshots__/demo.test.jsx.snap
index 232c1bb7..c0fa6e88 100644
--- a/src/image/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/image/__test__/__snapshots__/demo.test.jsx.snap
@@ -1679,6 +1679,591 @@ exports[`Image > Image mobileVue demo works fine 1`] = `
 </div>
 `;
 
+exports[`Image > Image positionVue demo works fine 1`] = `
+<div
+  class="image-example"
+  data-v-2d6651f5=""
+>
+  <div
+    class="image-example-title"
+    data-v-2d6651f5=""
+  >
+    不同填充位置的图片
+  </div>
+  <div
+    class="image-example-desc"
+    data-v-2d6651f5=""
+  >
+    当图片过大时，提供显示图片的局部左侧对齐、或右侧对齐的不同位置。
+  </div>
+  <div
+    class="image-group"
+    data-v-2d6651f5=""
+  >
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        cover center
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 72px; height: 72px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: cover; object-position: center;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        cover left
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 72px; height: 72px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: cover; object-position: left;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        cover right
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 72px; height: 72px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: cover; object-position: right;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        cover top
+      </p>
+      <div
+        class="t-image t-image--square"
+        data-v-2d6651f5=""
+        style="width: 240px; height: 80px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: cover; object-position: top;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        cover bottom
+      </p>
+      <div
+        class="t-image t-image--square"
+        data-v-2d6651f5=""
+        style="width: 240px; height: 80px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: cover; object-position: bottom;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        contain top
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 72px; height: 80px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: contain; object-position: top;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        contain bottom
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 72px; height: 80px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: contain; object-position: bottom;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        contain center
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 72px; height: 80px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: contain; object-position: center;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        contain left
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 180px; height: 80px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: contain; object-position: left;"
+          />
+        </picture>
+      </div>
+    </div>
+    <div
+      class="image-demo"
+      data-v-2d6651f5=""
+    >
+      <p
+        class="image-demo-tip"
+        data-v-2d6651f5=""
+      >
+        contain right
+      </p>
+      <div
+        class="t-image t-image--square image-container"
+        data-v-2d6651f5=""
+        style="width: 180px; height: 80px;"
+      >
+        <div
+          class="t-image__mask"
+        >
+          <div
+            class="t-loading"
+            style="font-size: 20px;"
+          >
+            
+            <div
+              class="t-loading__dots"
+              style="animation-duration: 800ms; width: 20px; height: 20px;"
+            >
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.26666666666666666s;"
+              />
+              <div
+                class="t-loading__dot"
+                style="animation-duration: 0.8s; animation-delay: 0.5333333333333333s;"
+              />
+            </div>
+            
+            <!--v-if-->
+            <!---->
+          </div>
+        </div>
+        <picture>
+          <!--v-if-->
+          <img
+            alt=""
+            class="t-image__img"
+            src="https://tdesign.gtimg.com/demo/demo-image-1.png"
+            style="object-fit: contain; object-position: right;"
+          />
+        </picture>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
 exports[`Image > Image shapeVue demo works fine 1`] = `
 <div
   class="image-example"
diff --git a/src/image/__test__/demo.test.jsx b/src/image/__test__/demo.test.jsx
index 79a7fed5..aab66c30 100644
--- a/src/image/__test__/demo.test.jsx
+++ b/src/image/__test__/demo.test.jsx
@@ -5,12 +5,14 @@
 import { mount } from '@vue/test-utils';
 import baseVue from '@/image/demos/base.vue';
 import mobileVue from '@/image/demos/mobile.vue';
+import positionVue from '@/image/demos/position.vue';
 import shapeVue from '@/image/demos/shape.vue';
 import statusVue from '@/image/demos/status.vue';
 
 const mapper = {
   baseVue,
   mobileVue,
+  positionVue,
   shapeVue,
   statusVue,
 };
diff --git a/src/popover/__test__/__snapshots__/demo.test.jsx.snap b/src/popover/__test__/__snapshots__/demo.test.jsx.snap
new file mode 100644
index 00000000..871492cf
--- /dev/null
+++ b/src/popover/__test__/__snapshots__/demo.test.jsx.snap
@@ -0,0 +1,2218 @@
+// Vitest Snapshot v1
+
+exports[`Popover > Popover mobileVue demo works fine 1`] = `
+<div
+  class="tdesign-mobile-demo"
+  data-v-c909e4b8=""
+>
+  <h1
+    class="title"
+    data-v-c909e4b8=""
+  >
+    Popover 弹出气泡
+  </h1>
+  <p
+    class="summary"
+    data-v-c909e4b8=""
+  >
+    用于文字提示的气泡框。
+  </p>
+  <div
+    class="tdesign-mobile-demo-block"
+    data-v-c909e4b8=""
+  >
+    <div
+      class="tdesign-mobile-demo-block__header"
+    >
+      <h2
+        class="tdesign-mobile-demo-block__title"
+      >
+        01 组件类型
+      </h2>
+      <!--v-if-->
+    </div>
+    <div
+      class="tdesign-mobile-demo-block__slot"
+    >
+      
+      
+      <div
+        class="popover-example"
+        data-v-3b645a88=""
+      >
+        <div
+          class="popover-example__summary mb-16"
+          data-v-3b645a88=""
+        >
+          带箭头的弹出气泡
+        </div>
+        <div
+          class="popover-example__content mb-24"
+          data-v-3b645a88=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle"
+              data-v-3b645a88=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                带箭头
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--dark"
+            >
+              弹出气泡内容
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+      </div>
+      <div
+        class="popover-example"
+        data-v-3b645a88=""
+      >
+        <div
+          class="popover-example__summary mb-16"
+          data-v-3b645a88=""
+        >
+          不带箭头的弹出气泡
+        </div>
+        <div
+          class="popover-example__content mb-24"
+          data-v-3b645a88=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle"
+              data-v-3b645a88=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                不带箭头
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--dark"
+            >
+              弹出气泡内容
+              <!--v-if-->
+            </div>
+          </div>
+          
+        </div>
+      </div>
+      <div
+        class="popover-example"
+        data-v-3b645a88=""
+      >
+        <div
+          class="popover-example__summary mb-16"
+          data-v-3b645a88=""
+        >
+          自定义内容弹出气泡
+        </div>
+        <div
+          class="custom popover-example__content mb-24"
+          data-v-3b645a88=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle"
+              data-v-3b645a88=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                自定义内容
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--dark"
+            >
+              
+              <div
+                class="custom__list"
+                data-v-3b645a88=""
+              >
+                
+                <div
+                  class="custom__item"
+                  data-v-3b645a88=""
+                >
+                  选项1
+                </div>
+                <div
+                  class="custom__item"
+                  data-v-3b645a88=""
+                >
+                  选项2
+                </div>
+                <div
+                  class="custom__item"
+                  data-v-3b645a88=""
+                >
+                  选项3
+                </div>
+                
+              </div>
+              
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+      </div>
+      
+      
+    </div>
+  </div>
+  <div
+    class="tdesign-mobile-demo-block"
+    data-v-c909e4b8=""
+  >
+    <div
+      class="tdesign-mobile-demo-block__header"
+    >
+      <h2
+        class="tdesign-mobile-demo-block__title"
+      >
+        02 组件样式
+      </h2>
+      <!--v-if-->
+    </div>
+    <div
+      class="tdesign-mobile-demo-block__slot"
+    >
+      
+      
+      <div
+        class="popover-example"
+        data-v-4c211b37=""
+      >
+        <div
+          class="popover-example__content"
+          data-v-4c211b37=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+              data-v-4c211b37=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                 深色 
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--dark"
+            >
+              弹出气泡内容
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+        <div
+          class="popover-example__content"
+          data-v-4c211b37=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+              data-v-4c211b37=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                 浅色 
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--light"
+            >
+              弹出气泡内容
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+        <div
+          class="popover-example__content"
+          data-v-4c211b37=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+              data-v-4c211b37=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                 品牌色 
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--brand"
+            >
+              弹出气泡内容
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+      </div>
+      <div
+        class="popover-example"
+        data-v-4c211b37=""
+      >
+        <div
+          class="popover-example__content"
+          data-v-4c211b37=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+              data-v-4c211b37=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                 成功色 
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--success"
+            >
+              弹出气泡内容
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+        <div
+          class="popover-example__content"
+          data-v-4c211b37=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+              data-v-4c211b37=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                 警告色 
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--warning"
+            >
+              弹出气泡内容
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+        <div
+          class="popover-example__content"
+          data-v-4c211b37=""
+        >
+          
+          <div
+            class="t-popover__wrapper"
+          >
+            
+            <button
+              aria-disabled="false"
+              class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+              data-v-4c211b37=""
+              role="button"
+              type="button"
+            >
+              <!---->
+              <span
+                class="t-button__content"
+              >
+                
+                 错误色 
+                
+              </span>
+              <!---->
+            </button>
+            
+          </div>
+          <div
+            class="t-popover"
+            data-popper-placement=""
+            style="display: none;"
+          >
+            <div
+              class="t-popover__content t-popover--error"
+            >
+              弹出气泡内容
+              <div
+                class="t-popover__arrow"
+              />
+            </div>
+          </div>
+          
+        </div>
+      </div>
+      
+      
+    </div>
+  </div>
+  <div
+    class="tdesign-mobile-demo-block tdesign-mobile-demo-block_notitle"
+    data-v-c909e4b8=""
+  >
+    <!--v-if-->
+    <div
+      class="tdesign-mobile-demo-block__slot"
+    >
+      
+      
+      <div
+        class="popover-example"
+        data-v-5b842f53=""
+      >
+        <div
+          class="popover-example__summary mb-16"
+          data-v-5b842f53=""
+        >
+          顶部弹出气泡
+        </div>
+        <div
+          class="popover-example__content row mb-24"
+          data-v-5b842f53=""
+        >
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   顶部左 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                弹出气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   顶部中 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                弹出气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   顶部右 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                弹出气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+        </div>
+      </div>
+      <div
+        class="popover-example"
+        data-v-5b842f53=""
+      >
+        <div
+          class="popover-example__summary mb-16"
+          data-v-5b842f53=""
+        >
+          底部弹出气泡
+        </div>
+        <div
+          class="popover-example__content row mb-24"
+          data-v-5b842f53=""
+        >
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   底部左 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                弹出气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   底部中 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                弹出气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   底部右 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                弹出气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+        </div>
+      </div>
+      <div
+        class="popover-example"
+        data-v-5b842f53=""
+      >
+        <div
+          class="popover-example__summary mb-16"
+          data-v-5b842f53=""
+        >
+          右侧弹出气泡
+        </div>
+        <div
+          class="popover-example__content column mb-24"
+          data-v-5b842f53=""
+        >
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   右侧上 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   右侧中 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   右侧下 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+        </div>
+      </div>
+      <div
+        class="popover-example"
+        data-v-5b842f53=""
+      >
+        <div
+          class="popover-example__summary mb-16"
+          data-v-5b842f53=""
+        >
+          左侧弹出气泡
+        </div>
+        <div
+          class="popover-example__content column flex-end mb-24"
+          data-v-5b842f53=""
+        >
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   左侧上 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   左侧中 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+          <div
+            class="popover-example__content__item"
+            data-v-5b842f53=""
+          >
+            
+            <div
+              class="t-popover__wrapper"
+            >
+              
+              <button
+                aria-disabled="false"
+                class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+                data-v-5b842f53=""
+                role="button"
+                type="button"
+              >
+                <!---->
+                <span
+                  class="t-button__content"
+                >
+                  
+                   左侧下 
+                  
+                </span>
+                <!---->
+              </button>
+              
+            </div>
+            <div
+              class="t-popover"
+              data-popper-placement=""
+              style="display: none;"
+            >
+              <div
+                class="t-popover__content t-popover--dark"
+              >
+                气泡内容
+                <div
+                  class="t-popover__arrow"
+                />
+              </div>
+            </div>
+            
+          </div>
+        </div>
+      </div>
+      
+      
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Popover > Popover placementVue demo works fine 1`] = `
+<div
+  data-v-app=""
+>
+  
+  <div
+    class="popover-example"
+    data-v-5b842f53=""
+  >
+    <div
+      class="popover-example__summary mb-16"
+      data-v-5b842f53=""
+    >
+      顶部弹出气泡
+    </div>
+    <div
+      class="popover-example__content row mb-24"
+      data-v-5b842f53=""
+    >
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               顶部左 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            弹出气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               顶部中 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            弹出气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               顶部右 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            弹出气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+    </div>
+  </div>
+  <div
+    class="popover-example"
+    data-v-5b842f53=""
+  >
+    <div
+      class="popover-example__summary mb-16"
+      data-v-5b842f53=""
+    >
+      底部弹出气泡
+    </div>
+    <div
+      class="popover-example__content row mb-24"
+      data-v-5b842f53=""
+    >
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               底部左 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            弹出气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               底部中 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            弹出气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               底部右 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            弹出气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+    </div>
+  </div>
+  <div
+    class="popover-example"
+    data-v-5b842f53=""
+  >
+    <div
+      class="popover-example__summary mb-16"
+      data-v-5b842f53=""
+    >
+      右侧弹出气泡
+    </div>
+    <div
+      class="popover-example__content column mb-24"
+      data-v-5b842f53=""
+    >
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               右侧上 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               右侧中 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               右侧下 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+    </div>
+  </div>
+  <div
+    class="popover-example"
+    data-v-5b842f53=""
+  >
+    <div
+      class="popover-example__summary mb-16"
+      data-v-5b842f53=""
+    >
+      左侧弹出气泡
+    </div>
+    <div
+      class="popover-example__content column flex-end mb-24"
+      data-v-5b842f53=""
+    >
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               左侧上 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               左侧中 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+      <div
+        class="popover-example__content__item"
+        data-v-5b842f53=""
+      >
+        
+        <div
+          class="t-popover__wrapper"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-with--large"
+            data-v-5b842f53=""
+            role="button"
+            type="button"
+          >
+            <!---->
+            <span
+              class="t-button__content"
+            >
+              
+               左侧下 
+              
+            </span>
+            <!---->
+          </button>
+          
+        </div>
+        <div
+          class="t-popover"
+          data-popper-placement=""
+          style="display: none;"
+        >
+          <div
+            class="t-popover__content t-popover--dark"
+          >
+            气泡内容
+            <div
+              class="t-popover__arrow"
+            />
+          </div>
+        </div>
+        
+      </div>
+    </div>
+  </div>
+  
+</div>
+`;
+
+exports[`Popover > Popover themeVue demo works fine 1`] = `
+<div
+  data-v-app=""
+>
+  
+  <div
+    class="popover-example"
+    data-v-4c211b37=""
+  >
+    <div
+      class="popover-example__content"
+      data-v-4c211b37=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+          data-v-4c211b37=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+             深色 
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--dark"
+        >
+          弹出气泡内容
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+    <div
+      class="popover-example__content"
+      data-v-4c211b37=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+          data-v-4c211b37=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+             浅色 
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--light"
+        >
+          弹出气泡内容
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+    <div
+      class="popover-example__content"
+      data-v-4c211b37=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+          data-v-4c211b37=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+             品牌色 
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--brand"
+        >
+          弹出气泡内容
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+  </div>
+  <div
+    class="popover-example"
+    data-v-4c211b37=""
+  >
+    <div
+      class="popover-example__content"
+      data-v-4c211b37=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+          data-v-4c211b37=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+             成功色 
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--success"
+        >
+          弹出气泡内容
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+    <div
+      class="popover-example__content"
+      data-v-4c211b37=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+          data-v-4c211b37=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+             警告色 
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--warning"
+        >
+          弹出气泡内容
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+    <div
+      class="popover-example__content"
+      data-v-4c211b37=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle button-width--small"
+          data-v-4c211b37=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+             错误色 
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--error"
+        >
+          弹出气泡内容
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+  </div>
+  
+</div>
+`;
+
+exports[`Popover > Popover typeVue demo works fine 1`] = `
+<div
+  data-v-app=""
+>
+  
+  <div
+    class="popover-example"
+    data-v-3b645a88=""
+  >
+    <div
+      class="popover-example__summary mb-16"
+      data-v-3b645a88=""
+    >
+      带箭头的弹出气泡
+    </div>
+    <div
+      class="popover-example__content mb-24"
+      data-v-3b645a88=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle"
+          data-v-3b645a88=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+            带箭头
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--dark"
+        >
+          弹出气泡内容
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+  </div>
+  <div
+    class="popover-example"
+    data-v-3b645a88=""
+  >
+    <div
+      class="popover-example__summary mb-16"
+      data-v-3b645a88=""
+    >
+      不带箭头的弹出气泡
+    </div>
+    <div
+      class="popover-example__content mb-24"
+      data-v-3b645a88=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle"
+          data-v-3b645a88=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+            不带箭头
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--dark"
+        >
+          弹出气泡内容
+          <!--v-if-->
+        </div>
+      </div>
+      
+    </div>
+  </div>
+  <div
+    class="popover-example"
+    data-v-3b645a88=""
+  >
+    <div
+      class="popover-example__summary mb-16"
+      data-v-3b645a88=""
+    >
+      自定义内容弹出气泡
+    </div>
+    <div
+      class="custom popover-example__content mb-24"
+      data-v-3b645a88=""
+    >
+      
+      <div
+        class="t-popover__wrapper"
+      >
+        
+        <button
+          aria-disabled="false"
+          class="t-button t-button--size-large t-button--outline t-button--primary t-button--rectangle"
+          data-v-3b645a88=""
+          role="button"
+          type="button"
+        >
+          <!---->
+          <span
+            class="t-button__content"
+          >
+            
+            自定义内容
+            
+          </span>
+          <!---->
+        </button>
+        
+      </div>
+      <div
+        class="t-popover"
+        data-popper-placement=""
+        style="display: none;"
+      >
+        <div
+          class="t-popover__content t-popover--dark"
+        >
+          
+          <div
+            class="custom__list"
+            data-v-3b645a88=""
+          >
+            
+            <div
+              class="custom__item"
+              data-v-3b645a88=""
+            >
+              选项1
+            </div>
+            <div
+              class="custom__item"
+              data-v-3b645a88=""
+            >
+              选项2
+            </div>
+            <div
+              class="custom__item"
+              data-v-3b645a88=""
+            >
+              选项3
+            </div>
+            
+          </div>
+          
+          <div
+            class="t-popover__arrow"
+          />
+        </div>
+      </div>
+      
+    </div>
+  </div>
+  
+</div>
+`;
diff --git a/src/popover/__test__/demo.test.jsx b/src/popover/__test__/demo.test.jsx
new file mode 100644
index 00000000..b6b732b2
--- /dev/null
+++ b/src/popover/__test__/demo.test.jsx
@@ -0,0 +1,25 @@
+/**
+ * 该文件为由脚本 `npm run test:demo` 自动生成，如需修改，执行脚本命令即可。请勿手写直接修改，否则会被覆盖
+ */
+
+import { mount } from '@vue/test-utils';
+import mobileVue from '@/popover/demos/mobile.vue';
+import placementVue from '@/popover/demos/placement.vue';
+import themeVue from '@/popover/demos/theme.vue';
+import typeVue from '@/popover/demos/type.vue';
+
+const mapper = {
+  mobileVue,
+  placementVue,
+  themeVue,
+  typeVue,
+};
+
+describe('Popover', () => {
+  Object.keys(mapper).forEach((demoName) => {
+    it(`Popover ${demoName} demo works fine`, () => {
+      const wrapper = mount(mapper[demoName]);
+      expect(wrapper.element).toMatchSnapshot();
+    });
+  });
+});
diff --git a/src/tag/__test__/__snapshots__/demo.test.jsx.snap b/src/tag/__test__/__snapshots__/demo.test.jsx.snap
index 6c7dc6ca..bd7232d0 100644
--- a/src/tag/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/tag/__test__/__snapshots__/demo.test.jsx.snap
@@ -37,7 +37,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             未选中态
             
           </span>
-          <!--v-if-->
         </span>
         <span
           aria-disabled="false"
@@ -56,7 +55,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             已选中态
             
           </span>
-          <!--v-if-->
         </span>
       </div>
       <div
@@ -84,7 +82,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             未选中态
             
           </span>
-          <!--v-if-->
         </span>
         <span
           aria-disabled="false"
@@ -103,7 +100,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             已选中态
             
           </span>
-          <!--v-if-->
         </span>
       </div>
       <div
@@ -131,7 +127,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             未选中态
             
           </span>
-          <!--v-if-->
         </span>
         <span
           aria-disabled="false"
@@ -150,7 +145,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             已选中态
             
           </span>
-          <!--v-if-->
         </span>
       </div>
       <div
@@ -178,7 +172,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             未选中态
             
           </span>
-          <!--v-if-->
         </span>
         <span
           aria-disabled="false"
@@ -197,7 +190,6 @@ exports[`Tag > Tag checkableVue demo works fine 1`] = `
             已选中态
             
           </span>
-          <!--v-if-->
         </span>
       </div>
       
@@ -645,7 +637,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   未选中态
                   
                 </span>
-                <!--v-if-->
               </span>
               <span
                 aria-disabled="false"
@@ -664,7 +655,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   已选中态
                   
                 </span>
-                <!--v-if-->
               </span>
             </div>
             <div
@@ -692,7 +682,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   未选中态
                   
                 </span>
-                <!--v-if-->
               </span>
               <span
                 aria-disabled="false"
@@ -711,7 +700,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   已选中态
                   
                 </span>
-                <!--v-if-->
               </span>
             </div>
             <div
@@ -739,7 +727,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   未选中态
                   
                 </span>
-                <!--v-if-->
               </span>
               <span
                 aria-disabled="false"
@@ -758,7 +745,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   已选中态
                   
                 </span>
-                <!--v-if-->
               </span>
             </div>
             <div
@@ -786,7 +772,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   未选中态
                   
                 </span>
-                <!--v-if-->
               </span>
               <span
                 aria-disabled="false"
@@ -805,7 +790,6 @@ exports[`Tag > Tag mobileVue demo works fine 1`] = `
                   已选中态
                   
                 </span>
-                <!--v-if-->
               </span>
             </div>
             
diff --git a/src/tag/__test__/index.test.jsx b/src/tag/__test__/index.test.jsx
index 9341817c..10dd44d1 100644
--- a/src/tag/__test__/index.test.jsx
+++ b/src/tag/__test__/index.test.jsx
@@ -86,14 +86,15 @@ describe('Tag.vue', async () => {
   });
 
   it('close render', async () => {
-    const wrapper = mount(() => <Tag closable>{TEXT}</Tag>);
+    const onClose = vi.fn();
+    const wrapper = mount(() => <Tag closable onClose={onClose}>{TEXT}</Tag>);
     const com = wrapper.findComponent(Tag);
     expect(wrapper.classes()).toContain('t-tag--closable');
     const closeBtn = com.find('.t-tag__icon-close');
     expect(closeBtn.exists()).toBeTruthy();
 
     await closeBtn.trigger('click');
-    expect(com.emitted('close')).toBeTruthy();
+    expect(onClose).toBeCalledWith({ e: expect.any(MouseEvent)});
   });
 });
 
@@ -113,16 +114,6 @@ describe('CheckTag render', async () => {
     expect(wrapper.classes()).toContain('t-tag--checked');
   });
 
-  it('close render', async () => {
-    const wrapper = mount(() => <TCheckTag closable>{TEXT}</TCheckTag>);
-    const com = wrapper.findComponent(TCheckTag);
-    expect(wrapper.classes()).toContain('t-tag--closable');
-    const closeBtn = com.find('.t-tag__icon-close');
-    expect(closeBtn.exists()).toBeTruthy();
-    await closeBtn.trigger('click');
-    expect(com.emitted('close')).toBeTruthy();
-  });
-
   it('checked render', async () => {
     const wrapper = mount(() => <TCheckTag modelValue={true}>{TEXT}</TCheckTag>);
     expect(wrapper.classes()).toContain('t-tag--checked');
diff --git a/src/textarea/__test__/index.test.jsx b/src/textarea/__test__/index.test.jsx
index fbc2d6ca..841b8617 100644
--- a/src/textarea/__test__/index.test.jsx
+++ b/src/textarea/__test__/index.test.jsx
@@ -25,9 +25,10 @@ describe('Textarea.vue', () => {
     const wrapper = mount(
       <Textarea label="标题" v-model={value.value} maxcharacter={maxcharacter} onChange={onChange} />,
     );
+    expect(onChange).toBeCalledTimes(1);
     const el = wrapper.find('textarea').element;
     await simulateEvent(el, '一个汉字等于两个字符，超出会被剪切', 'input');
-    expect(onChange).toBeCalledTimes(1);
+    expect(onChange).toBeCalledTimes(2);
     expect(onChange).toHaveBeenCalledWith('一');
   });
 
@@ -35,9 +36,10 @@ describe('Textarea.vue', () => {
     const value = ref('');
     const onChange = vi.fn();
     const wrapper = mount(<Textarea label="标题" v-model={value.value} autosize onChange={onChange} />);
+    expect(onChange).toBeCalledTimes(1);
     const el = wrapper.find('textarea').element;
     await simulateEvent(el, '这里是一段很长很长很长的长文本，支持自动换行', 'input');
-    expect(onChange).toBeCalledTimes(1);
+    expect(onChange).toBeCalledTimes(2);
   });
 
   it(': placeholder', async () => {