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