l-floating-panel.uvue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <template>
  2. <!-- #ifdef APP-ANDROID || APP-IOS || APP-HARMONY || WEB -->
  3. <view class="l-floating-panel" :style="[styles]" ref="rootRef" @touchstart="onTouchstart"
  4. @touchmove.stop="onTouchmove"
  5. @touchend="onTouchend"
  6. @touchcancel="onTouchend"
  7. @transitionend="onTransitionend">
  8. <view class="l-floating-panel__header">
  9. <view
  10. class="l-floating-panel__header-bar"
  11. :style="[barStyle]">
  12. </view>
  13. </view>
  14. <slot></slot>
  15. </view>
  16. <!-- #endif -->
  17. <!-- #ifndef APP-ANDROID || APP-IOS || APP-HARMONY || WEB -->
  18. <movable-area class="l-floating-panel-area" :style="[areaStyles]" :data-initialized="initialized">
  19. <movable-view
  20. class="l-floating-panel"
  21. direction="vertical"
  22. inertia
  23. out-of-bounds
  24. :damping="80"
  25. :friction="100"
  26. :disabled="isDraggable"
  27. :y="currentY"
  28. :animation="isAnimation"
  29. @change="onTouchmove"
  30. @touchstart="onTouchstart"
  31. @touchend="onTouchend"
  32. @touchcancel="onTouchend"
  33. :style="[styles]">
  34. <view class="l-floating-panel__header" data-handle="true">
  35. <view class="l-floating-panel__header-bar" :style="[barStyle]"></view>
  36. </view>
  37. <view class="l-floating-panel__content">
  38. <slot></slot>
  39. </view>
  40. </movable-view>
  41. </movable-area>
  42. <!-- #endif -->
  43. </template>
  44. <script lang="uts" setup>
  45. import { addUnit } from '@/uni_modules/lime-shared/addUnit';
  46. import { closest } from '@/uni_modules/lime-shared/closest';
  47. import { unitConvert } from '@/uni_modules/lime-shared/unitConvert';
  48. import { LFloatingPanelBoundary , FloatingPanelProps} from './type'
  49. // #ifdef APP-ANDROID || APP-IOS || APP-HARMONY || WEB
  50. import { useTouch } from './useTouch'
  51. // #endif
  52. const emit = defineEmits(['heightChange', 'change', 'update:height'])
  53. const props = withDefaults(defineProps<FloatingPanelProps>(), {
  54. height: 0,
  55. anchors: [] as number[],
  56. defaultAnchor: 0,
  57. animation: true,
  58. duration: 300,
  59. contentDraggable: true,
  60. safeAreaInsetBottom: true
  61. })
  62. // const height = defineModel('height', {type: Number, default: 0})
  63. let info = uni.getWindowInfo()
  64. const windowHeight = ref(info.windowHeight)
  65. const safeAreaInsets = ref(info.safeAreaInsets)
  66. let dragging = ref(false);
  67. let initialized = ref(false)
  68. const boundary = computed(() : LFloatingPanelBoundary => {
  69. const _anchors = props.anchors;
  70. const length = _anchors.length
  71. return {
  72. min: length > 0 ? _anchors[0] : 100,
  73. max: length > 0 ? _anchors[length - 1] : Math.round(windowHeight.value * 0.6),
  74. } as LFloatingPanelBoundary
  75. })
  76. const anchors = computed(() : number[] => {
  77. return props.anchors.length >= 2 ? props.anchors : [boundary.value.min, boundary.value.max]
  78. })
  79. const styles = computed(() : Map<string, any> => {
  80. const style = new Map<string, any>()
  81. style.set('height', `${boundary.value.max}px`)
  82. if(props.bgColor != null) {
  83. style.set('background-color', props.bgColor!)
  84. }
  85. return style
  86. })
  87. const barStyle = computed(() : Map<string, any> => {
  88. const style = new Map<string, any>()
  89. if(props.barColor != null) {
  90. style.set('background-color', props.barColor!)
  91. }
  92. return style
  93. })
  94. // #ifdef APP-ANDROID || APP-IOS || APP-HARMONY || WEB
  95. const touch = useTouch()
  96. const rootRef = ref<UniElement | null>(null)
  97. const contentRef = ref<UniElement | null>(null)
  98. const jumpAnchor = ref(0)
  99. const DAMP = 0.2;
  100. const height = ref(0);
  101. let startY = 0;
  102. let maxScroll = -1;
  103. const ease = (moveY : number) : number => {
  104. const absDistance = Math.abs(moveY);
  105. const { min, max } = boundary.value;
  106. if (absDistance > max) {
  107. return -(max + (absDistance - max) * DAMP);
  108. }
  109. if (absDistance < min) {
  110. return -(min - (min - absDistance) * DAMP);
  111. }
  112. return moveY;
  113. };
  114. const onTouchstart = (e : UniTouchEvent) => {
  115. touch.start(e);
  116. dragging.value = true;
  117. startY = -height.value;
  118. maxScroll = -1;
  119. }
  120. const onTouchmove = (e : UniTouchEvent) => {
  121. touch.move(e);
  122. const target = e.target!
  123. const classNmae = target.classList.length > 0 ? target.classList[0] : '';
  124. // 只有拖动了内容区域才进行判断是否需要阻止滚动
  125. if (!['l-floating-panel__header', 'l-floating-panel__header-bar'].includes(classNmae) && contentRef.value != null) {
  126. let scrollTop = 0;
  127. if(contentRef.value!.tagName != 'VIEW') {
  128. scrollTop = contentRef.value!.scrollTop;
  129. }
  130. maxScroll = Math.max(maxScroll, scrollTop);
  131. if (!props.contentDraggable) return;
  132. if (-startY < boundary.value.max) {
  133. e.preventDefault()
  134. e.stopPropagation()
  135. }
  136. else if (!(scrollTop <= 0 && touch.deltaY.value > 0) || maxScroll > 0) {
  137. return;
  138. }
  139. }
  140. //touch.deltaY.value 向上负 向下正
  141. const moveY = touch.deltaY.value + startY;
  142. height.value = -ease(moveY);
  143. }
  144. const onTouchend = (_ : UniTouchEvent) => {
  145. maxScroll = -1;
  146. dragging.value = false;
  147. height.value = closest(anchors.value, height.value);
  148. if (height.value != -startY) {
  149. emit('heightChange', { height: height.value });
  150. }
  151. }
  152. const onTransitionend = (_ : UniEvent) => {
  153. const index = anchors.value.findIndex((item : number) : boolean => item == height.value)
  154. if (index >= 0) {
  155. jumpAnchor.value = index
  156. }
  157. }
  158. const update = (value: number) => {
  159. if (rootRef.value == null) return
  160. rootRef.value!.style.setProperty('transition-duration', !dragging.value && initialized.value ? `${props.duration}ms` : '0ms')
  161. if(!dragging.value && initialized.value) {
  162. // 安卓要延时一下
  163. nextTick(() => {
  164. rootRef.value!.style.setProperty('transform', `translateY(${addUnit(boundary.value.max - value)})`)
  165. })
  166. } else {
  167. rootRef.value!.style.setProperty('transform', `translateY(${addUnit(boundary.value.max - value)})`)
  168. }
  169. emit('update:height', value)
  170. }
  171. const stopWatchHeight = watch(height, update)
  172. // const stopWatchBoundary = watch(boundary, (_ : LFloatingPanelBoundary) => {
  173. // height.value = closest(anchors.value, props.defaultAnchor == 0 ? height.value : anchors.value[props.defaultAnchor]);
  174. // })
  175. const stopWatchJumpAnchor = watch(jumpAnchor, (index : number) => {
  176. height.value = anchors.value[index]
  177. })
  178. onMounted(() => {
  179. nextTick(() => {
  180. // 鸿蒙无法在setup阶段获取到信息
  181. const res = uni.getWindowInfo();
  182. windowHeight.value = res.windowHeight
  183. safeAreaInsets.value = res.safeAreaInsets
  184. if (props.safeAreaInsetBottom && rootRef.value != null) {
  185. rootRef.value!.style.setProperty('padding-bottom', addUnit( unitConvert('150rpx') + safeAreaInsets.value.bottom))
  186. }
  187. // 查找插槽中的元素节点
  188. if (rootRef.value != null) {
  189. const lastChild = rootRef.value!.children[rootRef.value!.children.length - 1]
  190. if (lastChild.tagName != 'COMMENT') {
  191. contentRef.value = lastChild
  192. } else if (lastChild.previousSibling?.tagName != 'COMMENT') {
  193. contentRef.value = lastChild.previousSibling
  194. }
  195. }
  196. height.value = closest(anchors.value, props.defaultAnchor == 0 ? height.value : anchors.value[props.defaultAnchor]);
  197. update(height.value)
  198. nextTick(() => {
  199. // 首次不使用动画
  200. initialized.value = true
  201. })
  202. // let { windowHeight ,safeAreaInsets } = uni.getWindowInfo()
  203. })
  204. })
  205. // #endif
  206. // #ifndef APP-ANDROID || APP-IOS || APP-HARMONY || WEB
  207. const areaStyles = computed(() => {
  208. return ({
  209. height: addUnit(boundary.value.max * 2 - boundary.value.min),
  210. bottom: addUnit(boundary.value.max * -1 + boundary.value.min +(props.safeAreaInsetBottom ? safeAreaInsets.value.bottom:0)),
  211. opacity: initialized.value ? 1 : 0
  212. })
  213. })
  214. const calcY = (y: number):number => boundary.value.max - y;
  215. let moveYs = []
  216. let startY = 0
  217. const isAnimation = ref(false)
  218. const currentY = ref(calcY(props.anchors[props.defaultAnchor]) ?? calcY(boundary.value.min))
  219. const isDraggable = ref(!props.contentDraggable)
  220. const onTouchstart = (e: WechatMiniprogram.TouchEvent) => {
  221. startY = e.touches[0].clientY
  222. dragging.value = true
  223. moveYs.length = 0
  224. const { handle } = e.target.dataset
  225. if(!props.contentDraggable && Boolean(handle)) {
  226. isDraggable.value = false
  227. return
  228. }
  229. }
  230. const onTouchmove = (e: WechatMiniprogram.MovableViewChange) => {
  231. const {y} = e.detail
  232. if(dragging.value) {
  233. moveYs.push(y)
  234. }
  235. const height = calcY(y)
  236. emit('update:height',height)
  237. }
  238. const setCurrentY = (target: number) => {
  239. // currentY.value = target + 0.1;
  240. currentY.value = target
  241. const height = calcY(target)
  242. let index = anchors.value.findIndex(item => item == height)
  243. emit('heightChange', { height });
  244. emit('change', { height, index });
  245. }
  246. const reDraggable = () => {
  247. if(!props.contentDraggable) {
  248. setTimeout(() => {
  249. isDraggable.value = true
  250. }, 50);
  251. }
  252. }
  253. const onTouchend = (e: WechatMiniprogram.TouchEvent) => {
  254. let moveY = 0
  255. dragging.value = false
  256. const { handle } = e.target.dataset
  257. const isClick = Math.abs(e.changedTouches[0].clientY - startY) < 10
  258. if(isClick && !Boolean(handle)) {
  259. reDraggable()
  260. return
  261. }
  262. if(isClick) {
  263. const index = anchors.value.findIndex(item => item == calcY(currentY.value)) + 1
  264. // setCurrentY(calcY(anchors.value[index % anchors.value.length]))
  265. toAnchor(index % anchors.value.length)
  266. reDraggable()
  267. return
  268. } else if(moveYs.length) {
  269. moveY = moveYs[moveYs.length-1]
  270. }
  271. moveYs.length = 0
  272. reDraggable()
  273. setCurrentY(calcY(closest(anchors.value, calcY(moveY))))
  274. }
  275. const stopWatch = watch(() => props.anchors, () => {
  276. const index = anchors.value.findIndex(item => item == calcY(currentY.value)) + 1
  277. toAnchor(index)
  278. })
  279. onMounted(() => {
  280. isAnimation.value = props.animation
  281. setTimeout(() => {
  282. initialized.value = true
  283. }, 50);
  284. })
  285. onUnmounted(() => {
  286. stopWatch()
  287. })
  288. // #endif
  289. defineExpose({
  290. toAnchor: (index : number) => {
  291. if(index >= 0 && index < anchors.value.length) {
  292. // #ifndef APP-ANDROID || APP-IOS || APP-HARMONY || WEB
  293. setCurrentY(calcY(anchors.value[index]))
  294. // #endif
  295. // #ifdef APP-ANDROID || APP-IOS || APP-HARMONY || WEB
  296. jumpAnchor.value = index
  297. // #endif
  298. }
  299. }
  300. })
  301. </script>
  302. <style lang="scss">
  303. /* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY || WEB */
  304. @import './index-u.scss';
  305. /* #endif */
  306. /* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY || WEB */
  307. @import './index.scss';
  308. /* #endif */
  309. </style>