BottomSheet.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <template>
  2. <Popup
  3. v-bind="props"
  4. position="bottom"
  5. round
  6. :size="`${currentHeight}px`"
  7. :show="show"
  8. :noTransition="touching"
  9. @close="onCancelClick"
  10. >
  11. <FlexCol
  12. position="relative"
  13. backgroundColor="white"
  14. :innerStyle="{
  15. ...themeStyles.bottomSheet.value,
  16. height: `100%`,
  17. width: `100%`,
  18. ...innerStyle,
  19. }"
  20. >
  21. <view
  22. v-if="enableDrag"
  23. :style="themeStyles.dragHandleContainer.value"
  24. @touchstart="onDragStart"
  25. @touchmove="onDragMove"
  26. @touchend="onDragEnd"
  27. >
  28. <view
  29. :style="{
  30. ...themeStyles.dragHandle.value,
  31. width: themeContext.resolveSize(dragHandleSize),
  32. backgroundColor: themeContext.resolveThemeColor(dragHandleColor),
  33. }"
  34. />
  35. </view>
  36. <scroll-view
  37. :scroll-y="true"
  38. :style="{
  39. height: `100%`,
  40. }"
  41. >
  42. <slot name="content" :close="onCancelClick" />
  43. </scroll-view>
  44. </FlexCol>
  45. </Popup>
  46. </template>
  47. <script setup lang="ts">
  48. import { ref } from 'vue';
  49. import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine';
  50. import { DynamicColor, DynamicSize } from '../theme/ThemeTools';
  51. import Popup from './Popup.vue';
  52. import FlexCol from '../layout/FlexCol.vue';
  53. import type { PopupProps } from './Popup.vue';
  54. export interface BottomSheetProps extends Omit<PopupProps, 'onClose'|'position'|'position'|'size'> {
  55. /**
  56. * 是否显示
  57. * @default true
  58. */
  59. show: boolean;
  60. /**
  61. * 拖动把手颜色
  62. * @default 'grey'
  63. */
  64. dragHandleColor?: string;
  65. /**
  66. * 拖动把手大小
  67. * @default 100
  68. */
  69. dragHandleSize?: number;
  70. /**
  71. * 拖动时使高度吸附到指定高度
  72. * @default undefined
  73. */
  74. dragSnapHeights?: number[]|undefined;
  75. /**
  76. * 是否启用拖动
  77. * @default true
  78. */
  79. enableDrag?: boolean;
  80. /**
  81. * 底部弹窗大小(px)
  82. * @default '300
  83. */
  84. height?: number;
  85. /**
  86. * 拖动最大高度(px)
  87. * @default 1000
  88. */
  89. dragMaxHeight?: number;
  90. /**
  91. * 拖动最小高度(px)
  92. * @default 100
  93. */
  94. dragMinHeight?: number;
  95. innerStyle?: ViewStyle;
  96. }
  97. export interface BottomSheetExpose {
  98. setDragHeight: (height: number) => void;
  99. getDragHeight: () => number;
  100. setDragHeightToMax: () => void;
  101. setDragHeightToMin: () => void;
  102. }
  103. const emit = defineEmits([ 'close', 'select' ]);
  104. const props = withDefaults(defineProps<BottomSheetProps>(), {
  105. enableDrag: true,
  106. dragHandleColor: () => propGetThemeVar('BottomSheetDragHandleColor', 'grey'),
  107. dragHandleSize: () => propGetThemeVar('BottomSheetDragHandleSize', 200),
  108. centerWidth: () => propGetThemeVar('BottomSheetCenterWidth', '600rpx'),
  109. height: () => propGetThemeVar('BottomSheetHeight', 300),
  110. dragMaxHeight: () => propGetThemeVar('BottomSheetDragMaxHeight', 1000),
  111. dragMinHeight: () => propGetThemeVar('BottomSheetDragMinHeight', 100),
  112. });
  113. const themeContext = useTheme();
  114. const themeStyles = themeContext.useThemeStyles({
  115. bottomSheet: {
  116. borderTopLeftRadius: DynamicSize('BottomSheetBorderRadius', 10),
  117. borderTopRightRadius: DynamicSize('BottomSheetBorderRadius', 10),
  118. backgroundColor: DynamicColor('BottomSheetBackgroundColor', 'white'),
  119. },
  120. dragHandleContainer: {
  121. display: 'flex',
  122. justifyContent: 'center',
  123. alignItems: 'center',
  124. },
  125. dragHandle: {
  126. height: DynamicSize('BottomSheetDragHandleSize', 10),
  127. borderRadius: DynamicSize('BottomSheetDragHandleBorderRadius', 10),
  128. marginTop: DynamicSize('BottomSheetDragHandleMarginVertical', 25),
  129. marginBottom: DynamicSize('BottomSheetDragHandleMarginVertical', 25),
  130. },
  131. });
  132. const currentHeight = ref(props.height);
  133. const touching = ref(false);
  134. let touchStartY = 0;
  135. let touchStartHeight = 0;
  136. function onDragStart(e: TouchEvent) {
  137. if (!props.enableDrag) return;
  138. e.preventDefault();
  139. e.stopPropagation();
  140. touchStartY = e.touches[0].clientY;
  141. touchStartHeight = currentHeight.value;
  142. touching.value = true;
  143. }
  144. function onDragMove(e: TouchEvent) {
  145. if (!props.enableDrag || !touching.value) return;
  146. e.preventDefault();
  147. e.stopPropagation();
  148. const deltaY = e.touches[0].clientY - touchStartY;
  149. currentHeight.value = Math.max(props.dragMinHeight,
  150. Math.min(props.dragMaxHeight, touchStartHeight - deltaY)
  151. );
  152. }
  153. function onDragEnd(e: TouchEvent) {
  154. if (!props.enableDrag) return;
  155. e.preventDefault();
  156. e.stopPropagation();
  157. touching.value = false;
  158. //吸附到指定高度
  159. if (props.dragSnapHeights) {
  160. // 使当前高度吸附到最近的指定高度
  161. const snapHeights = props.dragSnapHeights.slice().sort((a, b) => a - b);
  162. let nearest = snapHeights[0];
  163. let minDist = Math.abs(currentHeight.value - snapHeights[0]);
  164. for (let i = 1; i < snapHeights.length; i++) {
  165. const dist = Math.abs(currentHeight.value - snapHeights[i]);
  166. if (dist < minDist) {
  167. minDist = dist;
  168. nearest = snapHeights[i];
  169. }
  170. }
  171. currentHeight.value = nearest;
  172. }
  173. }
  174. function onCancelClick() {
  175. emit('close');
  176. }
  177. defineExpose<BottomSheetExpose>({
  178. setDragHeight: (height: number) => {
  179. currentHeight.value = height;
  180. },
  181. setDragHeightToMax: () => {
  182. currentHeight.value = props.dragMaxHeight;
  183. },
  184. setDragHeightToMin: () => {
  185. currentHeight.value = props.dragMinHeight;
  186. },
  187. getDragHeight: () => {
  188. return currentHeight.value;
  189. },
  190. });
  191. </script>