hqs-popup.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <style lang="scss">
  2. .hqs-popup {
  3. .qs-con {
  4. background: #fff;
  5. }
  6. .qs-header {
  7. display: flex;
  8. align-items: center;
  9. justify-content: space-between;
  10. position: relative;
  11. }
  12. .qs-title {
  13. font-size: 16px;
  14. font-weight: bold;
  15. overflow: hidden;
  16. white-space: nowrap;
  17. text-overflow: ellipsis;
  18. }
  19. .hidden { visibility: hidden; }
  20. .qs-side {
  21. min-width: 60px;
  22. padding: 15px 20px;
  23. color: #888;
  24. flex-shrink: 0;
  25. &:active { opacity: .8; }
  26. }
  27. .qs-h-scroll {
  28. height: 100vh;
  29. // #ifdef H5
  30. margin-top: 44px;
  31. height: calc(100vh - 44px);
  32. // #endif
  33. }
  34. .ta-r { text-align: right; }
  35. }
  36. </style>
  37. <template>
  38. <uni-popup ref="popup"
  39. :mask-click="maskClick" @change="onChange"
  40. :type="from" class="hqs-popup">
  41. <view class="qs-con"
  42. :style="conStyle"
  43. @mousedown="onTouch" @mousemove="onTouch" @mouseup="onTouch"
  44. @touchstart="onTouch" @touchmove="onTouch" @touchend="onTouch">
  45. <block v-if="isVertical">
  46. <view class="qs-header" v-if="from == 'bottom' && showHeader">
  47. <view class="qs-side" :class="{'hidden': !showBack && !$slots.back}"
  48. @click="onBack">
  49. <slot name="back">
  50. <text>返回</text>
  51. </slot>
  52. </view>
  53. <slot name="title">
  54. <text class="qs-title">{{ title }}</text>
  55. </slot>
  56. <view class="qs-side ta-r" :class="{'hidden': !showClose}"
  57. @click="close">
  58. <slot name="close">
  59. <u-icon :name="closeIcon" :size="32" v-if="closeIcon"></u-icon>
  60. <text v-else>关闭</text>
  61. </slot>
  62. </view>
  63. </view>
  64. <slot name="sub-header"></slot>
  65. <scroll-view scroll-y :style="{ height }"
  66. @scroll="onScroll">
  67. <view>
  68. <slot></slot>
  69. </view>
  70. </scroll-view>
  71. <slot name="bottom"></slot>
  72. </block>
  73. <block v-else-if="!isVertical">
  74. <scroll-view scroll-y class="qs-h-scroll" :style="{ width }">
  75. <slot></slot>
  76. </scroll-view>
  77. </block>
  78. </view>
  79. </uni-popup>
  80. </template>
  81. <script>
  82. import UniPopup from './uni-popup.vue'
  83. export default {
  84. components: {
  85. UniPopup,
  86. },
  87. props: {
  88. // 弹窗显示可通过v-model控制
  89. value: Boolean,
  90. // 弹窗打开开始方向
  91. from: {
  92. type: String,
  93. default: 'bottom',
  94. },
  95. // 内容区边缘圆角大小
  96. round: {
  97. type: Number,
  98. default: 10,
  99. },
  100. // 弹窗内容宽度(当from=left或right时起作用)
  101. width: {
  102. type: String,
  103. default: '60vw',
  104. },
  105. // 弹窗内容高度(当from=top或bottom时起作用)
  106. height: {
  107. type: String,
  108. default: '50vh',
  109. },
  110. // 显示默认头部标题栏(仅在底部弹出时有)
  111. showHeader: {
  112. type: Boolean,
  113. default: true,
  114. },
  115. // 弹窗标题
  116. title: String,
  117. // 显示左侧(返回)按钮,如果v-slot:back存在时,也会显示此按钮
  118. showBack: Boolean,
  119. // 显示关闭按钮,如果是字符串,将作为uview(需另外引入)的u-icon组件的name(如showClose="close",会显示close图标)
  120. showClose: {
  121. type: [Boolean, String],
  122. default: true,
  123. },
  124. // 是否可点击模态背景关闭弹窗
  125. maskClick: {
  126. type: Boolean,
  127. default: true,
  128. },
  129. },
  130. data() {
  131. return {
  132. scrollTop: 0,
  133. panStyle: '',
  134. showPopup: false,
  135. }
  136. },
  137. computed: {
  138. closeIcon() {
  139. if(typeof this.showClose == 'string') return this.showClose
  140. return false
  141. },
  142. isVertical() {
  143. return ['bottom', 'top'].includes(this.from)
  144. },
  145. conStyle() {
  146. let style = this.panStyle || ''
  147. let r = this.round
  148. if(r > 0) {
  149. r += 'px'
  150. if(this.from == 'bottom') r = [r, r, 0, 0]
  151. else if(this.from == 'left') r = [0, r, r, 0]
  152. else if(this.from == 'right') r = [r, 0, 0, r]
  153. else r = [0, 0, r, r]
  154. style += `border-radius: ${r.join(' ')};`
  155. }
  156. return style
  157. },
  158. },
  159. watch: {
  160. value(val) {
  161. if(val == this.showPopup) return
  162. if(val) this.open()
  163. else this.close()
  164. },
  165. showPopup(val) {
  166. this.$emit('input', val)
  167. },
  168. },
  169. mounted() {
  170. if(this.value) this.open()
  171. },
  172. methods: {
  173. onScroll(e) {
  174. this.scrollTop = e.detail.scrollTop
  175. },
  176. onTouch(ev) {
  177. // if(!this.maskClick) return
  178. const { pageX, pageY } = ev.changedTouches[0] || ev
  179. if(['touchstart', 'mousedown'].includes(ev.type)) {
  180. this.startX = pageX
  181. this.startY = pageY
  182. this.startTime = ev.timeStamp
  183. } else {
  184. if(!this.startTime) return
  185. const orien = this.isVertical ? 'Y' : 'X'
  186. let moveDis = pageY - this.startY
  187. if(this.from == 'left') moveDis = this.startX - pageX
  188. else if(this.from == 'right') moveDis = pageX - this.startX
  189. else if(this.from == 'top') moveDis = -moveDis
  190. if(!this.maskClick) moveDis /= 3
  191. const duration = (ev.timeStamp - this.startTime)
  192. const speed = moveDis/duration
  193. if(['touchend', 'mouseup'].includes(ev.type)) {
  194. if(this.panStyle) {
  195. if(this.maskClick && (moveDis > 120 || speed > 0.25)) {
  196. this.close()
  197. }
  198. else {
  199. this.panStyle = `transform: translate${orien}(0); transition: all ease 200ms;`
  200. }
  201. setTimeout(() => {
  202. this.panStyle = ''
  203. }, 300)
  204. }
  205. // conScrollTop = 0
  206. this.startTime = 0
  207. return
  208. }
  209. // if(this.scrollTop > 0) return
  210. if(moveDis > 0) {
  211. if(this.from == 'left' || this.from == 'top') moveDis *= -1
  212. this.panStyle = `transform: translate${orien}(${moveDis}px); transition: none;`
  213. }
  214. }
  215. },
  216. onChange({ show }) {
  217. this.showPopup = show
  218. },
  219. open() {
  220. this.$refs.popup.open()
  221. this.showPopup = true
  222. },
  223. close() {
  224. this.$refs.popup.close()
  225. this.showPopup = false
  226. },
  227. onBack() {
  228. this.$emit('back')
  229. },
  230. }
  231. }
  232. </script>