diff --git a/src/fab/fab.en-US.md b/src/fab/fab.en-US.md
index bf6ab2cc8..a2e72ddc6 100644
--- a/src/fab/fab.en-US.md
+++ b/src/fab/fab.en-US.md
@@ -10,6 +10,7 @@ buttonProps | Object | - | Typescript:`ButtonProps`,[Button API Documents](.
icon | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
style | String | right: 16px; bottom: 32px; | \- | N
text | String | - | \- | N
+draggable | Boolean | false | set draggable | N
onClick | Function | | Typescript:`(context: {e: MouseEvent}) => void`
| N
### Fab Events
diff --git a/src/fab/fab.md b/src/fab/fab.md
index f3defd30e..db4b734d0 100644
--- a/src/fab/fab.md
+++ b/src/fab/fab.md
@@ -10,6 +10,7 @@ buttonProps | Object | - | 透传至 Button 组件。TS 类型:`ButtonProps`
icon | Slot / Function | - | 图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
style | String | right: 16px; bottom: 32px; | 悬浮按钮的样式,常用于调整位置 | N
text | String | - | 文本内容 | N
+draggable | Boolean | false | 是否可拖动 | N
onClick | Function | | TS 类型:`(context: {e: MouseEvent}) => void`
悬浮按钮点击事件 | N
### Fab Events
diff --git a/src/fab/fab.tsx b/src/fab/fab.tsx
index cee02acc3..0961c6407 100644
--- a/src/fab/fab.tsx
+++ b/src/fab/fab.tsx
@@ -1,4 +1,4 @@
-import { defineComponent } from 'vue';
+import { defineComponent, ref, computed, onMounted, watch } from 'vue';
import config from '../config';
import FabProps from './props';
import { useTNodeJSX } from '../hooks/tnode';
@@ -15,15 +15,121 @@ export default defineComponent({
const renderTNodeJSX = useTNodeJSX();
const fabClass = usePrefixClass('fab');
+ const fabRef = ref();
const handleClick = (e: MouseEvent) => {
props.onClick?.({ e });
};
+ const mounted = ref(false);
+ const btnSwitchPos = ref({
+ x: 16,
+ y: 32,
+ });
+ const switchPos = ref({
+ hasMoved: false, // exclude click event
+ x: btnSwitchPos.value.x, // right
+ y: btnSwitchPos.value.y, // bottom
+ startX: 0,
+ startY: 0,
+ endX: 0,
+ endY: 0,
+ });
+
+ const onTouchStart = (e: TouchEvent) => {
+ switchPos.value.startX = e.touches[0].pageX;
+ switchPos.value.startY = e.touches[0].pageY;
+ };
+
+ const onTouchMove = (e: TouchEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (!props.draggable) {
+ return;
+ }
+
+ if (e.touches.length <= 0) {
+ return;
+ }
+ const offsetX = e.touches[0].pageX - switchPos.value.startX;
+ const offsetY = e.touches[0].pageY - switchPos.value.startY;
+ const x = Math.floor(switchPos.value.x - offsetX);
+ const y = Math.floor(switchPos.value.y - offsetY);
+ btnSwitchPos.value.x = x;
+ btnSwitchPos.value.y = y;
+ switchPos.value.endX = x;
+ switchPos.value.endY = y;
+ switchPos.value.hasMoved = true;
+ };
+
+ const onTouchEnd = (e: TouchEvent) => {
+ if (!switchPos.value.hasMoved) {
+ return;
+ }
+ switchPos.value.startX = 0;
+ switchPos.value.startY = 0;
+ switchPos.value.hasMoved = false;
+ setSwitchPosition(switchPos.value.endX, switchPos.value.endY);
+ };
+
+ const setSwitchPosition = (switchX: number, switchY: number) => {
+ switchPos.value.x = switchX;
+ switchPos.value.y = switchY;
+ btnSwitchPos.value.x = switchX;
+ btnSwitchPos.value.y = switchY;
+ };
+
+ const fabStyle = computed(() => ({
+ right: `${btnSwitchPos.value.x}px`,
+ bottom: `${btnSwitchPos.value.y}px`,
+ }));
+
+ onMounted(() => {
+ mounted.value = true;
+ resetDraggableParams();
+ });
+
+ const getFabOriginStyle = () => {
+ const info = window.getComputedStyle(fabRef.value);
+ const { right, bottom } = info || {};
+ const getNumber = (num: string) => num.replace(/[^\d]/g, '');
+
+ return {
+ right: +(getNumber(right) || 0),
+ bottom: +(getNumber(bottom) || 0),
+ };
+ };
+
+ const resetDraggableParams = () => {
+ const { right, bottom } = getFabOriginStyle();
+
+ btnSwitchPos.value.x = right;
+ btnSwitchPos.value.y = bottom;
+
+ switchPos.value.x = right;
+ switchPos.value.y = bottom;
+ };
+
+ watch(
+ () => props.style,
+ () => {
+ resetDraggableParams();
+ },
+ );
+
return () => {
const icon = () => renderTNodeJSX('icon');
return (
-