l-floating-panel.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. <template>
  2. <movable-area class="l-floating-panel-area" :style="[areaStyles]" :data-initialized="initialized">
  3. <movable-view
  4. class="l-floating-panel"
  5. direction="vertical"
  6. inertia
  7. out-of-bounds
  8. :damping="80"
  9. :friction="100"
  10. :disabled="isDraggable"
  11. :y="currentY"
  12. :animation="isAnimation"
  13. @change="onTouchmove"
  14. @touchstart="onTouchstart"
  15. @touchend="onTouchend"
  16. @touchcancel="onTouchend"
  17. :style="[styles]">
  18. <view class="l-floating-panel__header" data-handle="true">
  19. <view class="l-floating-panel__header-bar" :style="{'background': barColor}"></view>
  20. </view>
  21. <view class="l-floating-panel__content">
  22. <slot></slot>
  23. </view>
  24. </movable-view>
  25. </movable-area>
  26. </template>
  27. <script lang="ts">
  28. // @ts-nocheck
  29. import { defineComponent, computed, ref , onMounted, onUnmounted, watch} from '@/uni_modules/lime-shared/vue';
  30. import floatingPanelProps from './props'
  31. import {addUnit} from '@/uni_modules/lime-shared/addUnit';
  32. import {sleep} from '@/uni_modules/lime-shared/sleep';
  33. import {closest} from '@/uni_modules/lime-shared/closest';
  34. const name = 'l-floating-panel'
  35. /**
  36. * LimeFloatingPanel 浮动面板
  37. * @description 浮动在页面底部的面板,可以上下拖动来浏览内容
  38. * @tutorial https://ext.dcloud.net.cn/plugin?id=13407
  39. * @property {Number} height 插件返回的高度
  40. * @property {Array} anchors 设置自定义锚点 默认值 [100, windowHeight * 0.6]
  41. * @property {Number} defaultAnchor 设置开始锚点的下标
  42. * @property {Boolean} animation 是否开启动画
  43. * @property {Boolean} contentDraggable = true 允许拖拽内容容器 默认 true
  44. * @property {Boolean} safeAreaInsetBottom = true 是否开启底部安全区域 默认 true
  45. * @event {Function} heightChange 高度变化时触发
  46. */
  47. export default defineComponent({
  48. name,
  49. props: floatingPanelProps,
  50. emits: ['heightChange', 'change', 'update:height'],
  51. setup(props, {emit, expose}) {
  52. const {windowHeight, safeAreaInsets } = uni.getSystemInfoSync()
  53. const isDraggable = ref(!props.contentDraggable)
  54. const isAnimation = ref(false)
  55. const dragging = ref(false)
  56. const initialized = ref(false)
  57. const boundary = computed(() => {
  58. const anchors = props.anchors as number[]||[];
  59. return {
  60. min: anchors[0] ?? 100,
  61. max:
  62. anchors[anchors.length - 1] ??
  63. Math.round(windowHeight * 0.6),
  64. }
  65. });
  66. const calcY = (y: number) => boundary.value.max - y
  67. const anchors = computed(() => {
  68. const anchors = props.anchors as number[]||[];
  69. return anchors.length >= 2
  70. ? anchors
  71. : [boundary.value.min, boundary.value.max]
  72. })
  73. const areaStyles = computed(() => {
  74. return ({
  75. height: addUnit(boundary.value.max * 2 - boundary.value.min),
  76. bottom: addUnit(boundary.value.max * -1 + boundary.value.min +(props.safeAreaInsetBottom ? safeAreaInsets.bottom:0)),
  77. opacity: initialized.value ? 1 : 0
  78. })
  79. })
  80. const styles = computed(() => {
  81. return ({
  82. height: addUnit(boundary.value.max) ,
  83. background: props.baColor
  84. })
  85. })
  86. const currentY = ref(calcY(props.anchors[props.defaultAnchor]) ?? calcY(boundary.value.min))
  87. let moveYs = []
  88. let startY = 0
  89. const onTouchstart = (e: WechatMiniprogram.TouchEvent) => {
  90. startY = e.touches[0].clientY
  91. dragging.value = true
  92. moveYs.length = 0
  93. const { handle } = e.target.dataset
  94. if(!props.contentDraggable && Boolean(handle)) {
  95. isDraggable.value = false
  96. return
  97. }
  98. }
  99. const onTouchmove = (e: WechatMiniprogram.MovableViewChange) => {
  100. const {y} = e.detail
  101. if(dragging.value) {
  102. moveYs.push(y)
  103. }
  104. const height = calcY(y)
  105. emit('update:height',height)
  106. }
  107. const setCurrentY = (target: number) => {
  108. currentY.value = target + 0.1;
  109. // h5要延迟才能触发
  110. sleep(50).then(() => {
  111. currentY.value = target
  112. const height = calcY(target)
  113. let index = anchors.value.findIndex(item => item == height)
  114. emit('heightChange', { height });
  115. emit('change', { height, index });
  116. })
  117. }
  118. const toAnchor = (index: number) => {
  119. if(index >= 0 && index < anchors.value.length) {
  120. setCurrentY(calcY(anchors.value[index]))
  121. }
  122. }
  123. const reDraggable = () => {
  124. if(!props.contentDraggable) {
  125. sleep(50).then(() => isDraggable.value = true)
  126. }
  127. }
  128. const onTouchend = (e: WechatMiniprogram.TouchEvent) => {
  129. let moveY = 0
  130. dragging.value = false
  131. const { handle } = e.target.dataset
  132. const isClick = Math.abs(e.changedTouches[0].clientY - startY) < 10
  133. if(isClick && !Boolean(handle)) {
  134. reDraggable()
  135. return
  136. }
  137. if(isClick) {
  138. const index = anchors.value.findIndex(item => item == calcY(currentY.value)) + 1
  139. // setCurrentY(calcY(anchors.value[index % anchors.value.length]))
  140. toAnchor(index % anchors.value.length)
  141. reDraggable()
  142. return
  143. } else if(moveYs.length) {
  144. moveY = moveYs[moveYs.length-1]
  145. }
  146. moveYs.length = 0
  147. reDraggable()
  148. setCurrentY(calcY(closest(anchors.value, calcY(moveY))))
  149. }
  150. const stopWatch = watch(() => props.anchors, () => {
  151. const index = anchors.value.findIndex(item => item == calcY(currentY.value)) + 1
  152. toAnchor(index)
  153. })
  154. onMounted(() => {
  155. isAnimation.value = props.animation
  156. sleep(50).then(() => initialized.value = true)
  157. })
  158. onUnmounted(() => {
  159. stopWatch()
  160. })
  161. // #ifdef VUE3
  162. expose({
  163. toAnchor
  164. })
  165. // #endif
  166. return {
  167. initialized,
  168. areaStyles,
  169. styles,
  170. isDraggable,
  171. isAnimation,
  172. currentY,
  173. onTouchstart,
  174. onTouchmove,
  175. onTouchend,
  176. // #ifndef VUE3
  177. toAnchor
  178. // #endif
  179. }
  180. }
  181. })
  182. </script>
  183. <style lang="scss">
  184. @import './index.scss';
  185. </style>