25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

273 lines
9.3 KiB

  1. <template>
  2. <view class="u-dropdown">
  3. <view class="u-dropdown__menu" :style="{
  4. height: addUnit(height)
  5. }" :class="{
  6. 'u-border-bottom': borderBottom
  7. }">
  8. <view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
  9. <view class="u-flex u-flex-row">
  10. <text class="u-dropdown__menu__item__text" :style="{
  11. color: item.disabled ? menuDisabledColor : (index === current || highlightIndexList.includes(index)) ? resolvedActiveColor : resolvedInactiveColor,
  12. fontSize: addUnit(titleSize)
  13. }">{{item.title}}</text>
  14. <view class="u-dropdown__menu__item__arrow" :class="{
  15. 'u-dropdown__menu__item__arrow--rotate': index === current
  16. }">
  17. <up-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="addUnit(menuIconSize)" :color="index === current || highlightIndexList.includes(index) ? resolvedActiveColor : menuDisabledColor"></up-icon>
  18. </view>
  19. </view>
  20. </view>
  21. </view>
  22. <view class="u-dropdown__content" :style="[contentStyle, {
  23. transition: `opacity ${duration / 1000}s, z-index ${duration / 1000}s linear`,
  24. top: addUnit(height)
  25. }]"
  26. @tap="maskClick" @touchmove.stop.prevent>
  27. <view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle, {
  28. }]">
  29. <slot></slot>
  30. </view>
  31. <view class="u-dropdown__content__mask"></view>
  32. </view>
  33. </view>
  34. </template>
  35. <script>
  36. import { props } from './props';
  37. import { mpMixin } from '../../libs/mixin/mpMixin';
  38. import { mixin } from '../../libs/mixin/mixin';
  39. import { addUnit, getWindowInfo} from '../../libs/function/index';
  40. /**
  41. * dropdown 下拉菜单
  42. * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
  43. * @tutorial https://uview-plus.jiangruyi.com/components/dropdown.html
  44. * @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
  45. * @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
  46. * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
  47. * @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
  48. * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
  49. * @property {String | Number} height 标题菜单的高度,单位任意(默认80)
  50. * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
  51. * @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
  52. * @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
  53. * @event {Function} open 下拉菜单被打开时触发
  54. * @event {Function} close 下拉菜单被关闭时触发
  55. * @example <u-dropdown></u-dropdown>
  56. */
  57. export default {
  58. name: 'u-dropdown',
  59. mixins: [mpMixin, mixin, props],
  60. data() {
  61. return {
  62. showDropdown: true, // 是否打开下来菜单,
  63. menuList: [], // 显示的菜单
  64. active: false, // 下拉菜单的状态
  65. // 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
  66. // 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
  67. current: 99999,
  68. // 外层内容的样式,初始时处于底层,且透明
  69. contentStyle: {
  70. zIndex: -1,
  71. opacity: 0
  72. },
  73. // 让某些菜单保持高亮的状态
  74. highlightIndexList: [],
  75. contentHeight: 0
  76. }
  77. },
  78. computed: {
  79. resolvedActiveColor() {
  80. return this.activeColor === '#2979ff'
  81. ? this.upThemeVar('--up-primary', '#2979ff')
  82. : this.activeColor
  83. },
  84. resolvedInactiveColor() {
  85. return this.inactiveColor === '#606266'
  86. ? this.upThemeVar('--up-content-color', '#606266')
  87. : this.inactiveColor
  88. },
  89. menuDisabledColor() {
  90. return this.upThemeVar('--up-disabled-color', '#c0c4cc')
  91. },
  92. // 下拉出来部分的样式
  93. popupStyle() {
  94. let style = {};
  95. // 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
  96. style.transform = `translateY(${this.active ? 0 : '-100%'})`
  97. style['transition-duration'] = this.duration / 1000 + 's';
  98. style.borderRadius = `0 0 ${addUnit(this.borderRadius)} ${addUnit(this.borderRadius)}`;
  99. return style;
  100. }
  101. },
  102. created() {
  103. // 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
  104. this.children = [];
  105. },
  106. mounted() {
  107. this.getContentHeight();
  108. },
  109. emits: ['open', 'close'],
  110. methods: {
  111. addUnit,
  112. init() {
  113. // 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
  114. // 以保证数据的正确性
  115. this.menuList = [];
  116. this.children.map(child => {
  117. child.init();
  118. })
  119. },
  120. // 点击菜单
  121. menuClick(index) {
  122. // 判断是否被禁用
  123. if (this.menuList[index].disabled) return;
  124. // 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
  125. if (index === this.current && this.closeOnClickSelf) {
  126. this.close();
  127. // 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
  128. setTimeout(() => {
  129. this.children[index].active = false;
  130. }, this.duration)
  131. return;
  132. }
  133. this.open(index);
  134. },
  135. // 打开下拉菜单
  136. open(index) {
  137. // 嵌套popup使用时可能获取不到正确的高度,重新计算
  138. if (this.contentHeight < 1) this.getContentHeight()
  139. // 重置高亮索引,否则会造成多个菜单同时高亮
  140. // this.highlightIndex = 9999;
  141. // 展开时,设置下拉内容的样式
  142. this.contentStyle = {
  143. zIndex: 11,
  144. height: this.contentHeight + 'px'
  145. }
  146. // 标记展开状态以及当前展开项的索引
  147. this.active = true;
  148. this.current = index;
  149. // 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
  150. // 之所以不是因display: none,是因为nvue没有display这个属性
  151. this.children.map((val, idx) => {
  152. val.active = index == idx ? true : false;
  153. })
  154. this.$emit('open', this.current);
  155. },
  156. // 设置下拉菜单处于收起状态
  157. close() {
  158. this.$emit('close', this.current);
  159. // 设置为收起状态,同时current归位,设置为空字符串
  160. this.active = false;
  161. this.current = 99999;
  162. // 下拉内容的样式进行调整,不透明度设置为0
  163. this.contentStyle.zIndex = -1;
  164. this.contentStyle.opacity = 0;
  165. setTimeout(() => {
  166. this.contentStyle.height = 0;
  167. }, this.duration)
  168. },
  169. // 点击遮罩
  170. maskClick() {
  171. // 如果不允许点击遮罩,直接返回
  172. if (!this.closeOnClickMask) return;
  173. this.close();
  174. },
  175. // 外部手动设置某些菜单高亮
  176. highlight(indexParams = undefined) {
  177. if (Array.isArray(indexParams)) {
  178. this.highlightIndexList = [...indexParams];
  179. return;
  180. }
  181. this.highlightIndexList = indexParams !== undefined ? [indexParams] : [];
  182. },
  183. // 获取下拉菜单内容的高度
  184. getContentHeight() {
  185. // 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
  186. // 才能让遮罩占满菜单一下,直到屏幕底部的高度
  187. // getWindowInfo()为uview-plus封装的获取设备信息的方法
  188. let windowHeight = getWindowInfo().windowHeight;
  189. this.$uGetRect('.u-dropdown__menu').then(res => {
  190. // 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
  191. // H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
  192. // 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
  193. // 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
  194. this.contentHeight = windowHeight - res.bottom;
  195. })
  196. }
  197. }
  198. }
  199. </script>
  200. <style scoped lang="scss">
  201. .u-dropdown {
  202. flex: 1;
  203. width: 100%;
  204. position: relative;
  205. &__menu {
  206. @include flex;
  207. position: relative;
  208. z-index: 11;
  209. height: 80rpx;
  210. &__item {
  211. flex: 1;
  212. @include flex;
  213. justify-content: center;
  214. align-items: center;
  215. .u-flex-row {
  216. flex-direction: row;
  217. }
  218. &__text {
  219. font-size: 28rpx;
  220. color: $u-content-color;
  221. }
  222. &__arrow {
  223. margin-left: 6rpx;
  224. transition: transform .3s;
  225. align-items: center;
  226. @include flex;
  227. &--rotate {
  228. transform: rotate(180deg);
  229. }
  230. }
  231. }
  232. }
  233. &__content {
  234. position: absolute;
  235. z-index: 8;
  236. width: 100%;
  237. left: 0px;
  238. bottom: 0;
  239. overflow: hidden;
  240. &__mask {
  241. position: absolute;
  242. z-index: 9;
  243. background: rgba(0, 0, 0, .3);
  244. width: 100%;
  245. left: 0;
  246. top: 0;
  247. bottom: 0;
  248. }
  249. &__popup {
  250. position: relative;
  251. z-index: 10;
  252. transition: transform 0.3s;
  253. transform: translate3D(0, -100%, 0);
  254. overflow: hidden;
  255. }
  256. }
  257. }
  258. </style>