123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- <template>
- <view
- @touchmove.stop.prevent="handleTouchMove"
- @touchstart="handleTouchStart"
- @touchend="handleTouchEnd"
- class="u-fab"
- :class="[`u-fab--${this.position}`]"
- :style="[rootStyle]"
- @click.stop=""
- >
- <view :style="{ visibility: inited ? 'visible' : 'hidden' }" class="u-fab__trigger-wrapper">
- <slot name="trigger" v-if="$slots.trigger || $slots.$trigger"></slot>
- <u-button
- v-else
- shape="circle"
- @click="handleClick"
- :type="type"
- :disabled="disabled"
- :custom-style="triggerStyle"
- :icon="isActive ? activeIcon : inactiveIcon"
- ></u-button>
- </view>
-
- <u-transition
- v-if="expandable"
- :show="isActive"
- mode="fade"
- :duration="300"
- :custom-style="actionsStyle"
- >
- <view class="u-fab__actions" :class="[`u-fab__actions--${this.fabDirection}`]">
- <slot></slot>
- </view>
- </u-transition>
- </view>
- </template>
- <script>
- // #ifdef APP-NVUE
- const dom = uni.requireNativePlugin('dom')
- // #endif
- import props from './props.js';
- import mixin from '../../libs/mixin/mixin'
- import mpMixin from '../../libs/mixin/mpMixin';
- /**
- * fab 悬浮按钮
- * @description 悬浮动作按钮组件,按下可显示一组动作按钮
- * @tutorial https://uview.d3u.cn/components/fab.html
- * @property {Boolean} active 是否激活 (默认 false )
- * @property {String} type 类型,可选值为 primary、success、info、warning、error、default (默认 'primary' )
- * @property {String} position 悬浮按钮位置,可选值为 left-top、right-top、left-bottom、right-bottom、left-center、right-center、top-center、bottom-center (默认 'right-bottom' )
- * @property {String} draggable 按钮拖动模式,可选值为 auto(自动吸附)、free(自由拖动)、none(不可拖动) (默认 'auto' )
- * @property {String} direction 悬浮按钮菜单弹出方向,可选值为 top、right、bottom、left (默认 'top' )
- * @property {Boolean} disabled 是否禁用 (默认 false )
- * @property {String} inactiveIcon 悬浮按钮未展开时的图标 (默认 'plus' )
- * @property {String} activeIcon 悬浮按钮展开时的图标 (默认 'close' )
- * @property {String} iconColor 悬浮按钮图标颜色 (默认 '#fff' )
- * @property {Number} zIndex 自定义悬浮按钮层级 (默认 99 )
- * @property {Object} gap 自定义悬浮按钮与可视区域边缘的间距 (默认 {top: 16, left: 16, right: 16, bottom: 16} )
- * @property {Boolean} expandable 用于控制点击时是否展开菜单 (默认 true )
- * @property {Object} customStyle 定义需要用到的外部样式
- *
- * @event {Function} click expandable 设置为 false 时,点击悬浮按钮触发
- * @event {Function} change 菜单状态改变时触发
- * @event {Function} update:active 激活状态改变时触发
- * @example <u-fab v-model:active="active" :type="type" :position="position" :direction="direction"></u-fab>
- */
- export default {
- name: "u-fab",
- mixins: [mpMixin, mixin, props],
- data() {
- return {
- inited: false, // 是否初始化完成
- isActive: false, // 是否激活状态
- fabDirection: this.direction, // 实际弹出方向
- top: 0,
- left: 0,
- screen: { width: 0, height: 0 },
- fabSize: { width: 56, height: 56 },
- bounding: {
- minTop: 0,
- minLeft: 0,
- maxTop: 0,
- maxLeft: 0
- },
- touchOffset: { x: 0, y: 0 }, // 按下时坐标相对于元素的偏移量
- attractTransition: false // 是否显示吸附动画
- }
- },
- computed: {
- rootStyle() {
- const style = {
- position: 'fixed',
- top: this.$u.addUnit(this.top),
- left: this.$u.addUnit(this.left),
- zIndex: this.zIndex,
- transition: this.attractTransition ? 'all ease 0.3s' : 'none'
- }
- return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
- },
- triggerStyle() {
- return {
- width: this.$u.addUnit(this.size),
- height: this.$u.addUnit(this.size),
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)'
- }
- },
- actionsStyle() {
- const style = {
- position: 'absolute',
- zIndex: 1,
- display: 'flex',
- alignItems: 'center'
- }
- switch (this.fabDirection) {
- case 'top':
- style.bottom = '100%';
- style.left = '50%';
- style.transform = 'translateX(-50%)';
- style.flexDirection = 'column-reverse';
- break;
- case 'right':
- style.left = '100%';
- style.top = '50%';
- style.transform = 'translateY(-50%)';
- style.flexDirection = 'row';
- break;
- case 'bottom':
- style.top = '100%';
- style.left = '50%';
- style.transform = 'translateX(-50%)';
- style.flexDirection = 'column';
- break;
- case 'left':
- style.right = '100%';
- style.top = '50%';
- style.transform = 'translateY(-50%)';
- style.flexDirection = 'row-reverse';
- break;
- }
- return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
- }
- },
- watch: {
- active: {
- handler(newVal) {
- this.isActive = newVal
- },
- immediate: true
- },
- direction: {
- handler(newVal) {
- this.fabDirection = newVal
-
- }
- },
- position() {
- this.initPosition()
- }
- },
- mounted() {
- this.$nextTick(() => {
- this.init()
- })
- },
- // #ifdef VUE3
- emits: ['update:active', 'click','change'],
- // #endif
- methods: {
- async init() {
- await this.getBounding()
- this.initPosition()
- this.inited = true
- },
-
- async getBounding() {
- const windowInfo = this.$u.window()
-
- // 获取触发器尺寸
- try {
- // #ifndef APP-NVUE
- const triggerInfo = await this.$uGetRect('.u-fab__trigger-wrapper')
- this.fabSize.width = triggerInfo.width || 56
- this.fabSize.height = triggerInfo.height || 56
- // #endif
-
- // #ifdef APP-NVUE
- this.fabSize.width = 56
- this.fabSize.height = 56
- // #endif
- } catch (error) {
- console.log('获取触发器尺寸失败:', error)
- }
- const { top = 16, left = 16, right = 16, bottom = 16 } = this.gap
- this.screen.width = windowInfo.windowWidth
- this.screen.height = windowInfo.windowHeight
-
- // #ifdef H5
- this.screen.height = windowInfo.windowTop + windowInfo.windowHeight
- this.bounding.minTop = windowInfo.windowTop + top
- // #endif
-
- // #ifndef H5
- this.bounding.minTop = top
- // #endif
-
- this.bounding.minLeft = left
- this.bounding.maxLeft = this.screen.width - this.fabSize.width - right
- this.bounding.maxTop = this.screen.height - this.fabSize.height - bottom
- },
-
- initPosition() {
- const pos = this.position
- const { minLeft, minTop, maxLeft, maxTop } = this.bounding
- const centerY = (maxTop + minTop) / 2
- const centerX = (maxLeft + minLeft) / 2
- switch (pos) {
- case 'left-top':
- this.top = minTop
- this.left = minLeft
- break
- case 'right-top':
- this.top = minTop
- this.left = maxLeft
- break
- case 'left-bottom':
- this.top = maxTop
- this.left = minLeft
- break
- case 'right-bottom':
- this.top = maxTop
- this.left = maxLeft
- break
- case 'left-center':
- this.top = centerY
- this.left = minLeft
- break
- case 'right-center':
- this.top = centerY
- this.left = maxLeft
- break
- case 'top-center':
- this.top = minTop
- this.left = centerX
- break
- case 'bottom-center':
- this.top = maxTop
- this.left = centerX
- break
- }
- },
-
- handleTouchStart(e) {
- if (this.draggable === 'none') return
- const touch = e.touches[0]
- this.touchOffset.x = touch.clientX - this.left
- this.touchOffset.y = touch.clientY - this.top
- this.attractTransition = false
- },
- handleTouchMove(e) {
- if (this.draggable === 'none') return
- const touch = e.touches[0]
- const { minLeft, minTop, maxLeft, maxTop } = this.bounding
- let x = touch.clientX - this.touchOffset.x
- let y = touch.clientY - this.touchOffset.y
- if (x < minLeft) x = minLeft
- else if (x > maxLeft) x = maxLeft
- if (y < minTop) y = minTop
- else if (y > maxTop) y = maxTop
- this.top = y
- this.left = x
- },
- handleTouchEnd() {
- const screenCenterX = this.screen.width / 2
- const fabCenterX = this.left + this.fabSize.width / 2
- this.attractTransition = true
-
- // 自动吸附模式
- // 检查指定方向是否有足够空间
- const hasEnoughSpace = this.checkDirectionSpace(this.direction)
-
- // 自动计算最佳位置
- if (this.draggable === 'auto') {
- if (fabCenterX < screenCenterX) {
- this.left = this.bounding.minLeft
- } else {
- this.left = this.bounding.maxLeft
- }
- }
- if (hasEnoughSpace) {
- // 使用指定的方向
- this.fabDirection = this.direction
- this.adjustPositionForDirection(this.direction)
- } else {
- // 自动计算最佳位置
- if (fabCenterX < screenCenterX) {
- this.fabDirection = 'right'
- } else {
- this.fabDirection = 'left'
- }
- }
- },
-
- // 检查指定方向是否有足够空间
- checkDirectionSpace(direction) {
- const { minLeft, minTop, maxLeft, maxTop } = this.bounding
- const fabCenterX = this.left + this.fabSize.width / 2
- const fabCenterY = this.top + this.fabSize.height / 2
- const minSpaceNeeded = 100 // 菜单需要的最小空间
-
- switch (direction) {
- case 'top':
- return fabCenterY - minTop >= minSpaceNeeded
- case 'bottom':
- return maxTop - fabCenterY >= minSpaceNeeded
- case 'left':
- return fabCenterX - minLeft >= minSpaceNeeded
- case 'right':
- return maxLeft - fabCenterX >= minSpaceNeeded
- default:
- return false
- }
- },
-
- // 根据方向调整位置
- adjustPositionForDirection(direction) {
- const screenCenterX = this.screen.width / 2
- const screenCenterY = this.screen.height / 2
-
- switch (direction) {
- case 'top':
- case 'bottom':
- // 垂直方向时,水平居中或靠近边缘
- if (Math.abs(this.left + this.fabSize.width / 2 - screenCenterX) < 50) {
- // 如果接近中心,保持中心位置
- this.left = screenCenterX - this.fabSize.width / 2
- }
- break
- case 'left':
- case 'right':
- // 水平方向时,垂直居中或靠近边缘
- if (Math.abs(this.top + this.fabSize.height / 2 - screenCenterY) < 50) {
- // 如果接近中心,保持中心位置
- this.top = screenCenterY - this.fabSize.height / 2
- }
- break
- }
- },
-
- // 根据当前位置调整弹出方向(用于自由拖动模式)
- adjustDirectionByPosition() {
- const screenCenterX = this.screen.width / 2
- const screenCenterY = this.screen.height / 2
- const fabCenterX = this.left + this.fabSize.width / 2
- const fabCenterY = this.top + this.fabSize.height / 2
-
- // 根据位置智能选择弹出方向
- const distanceToLeft = fabCenterX - this.bounding.minLeft
- const distanceToRight = this.bounding.maxLeft - fabCenterX
- const distanceToTop = fabCenterY - this.bounding.minTop
- const distanceToBottom = this.bounding.maxTop - fabCenterY
-
- // 找到空间最大的方向
- const maxDistance = Math.max(distanceToLeft, distanceToRight, distanceToTop, distanceToBottom)
-
- if (maxDistance === distanceToTop) {
- this.fabDirection = 'top'
- } else if (maxDistance === distanceToBottom) {
- this.fabDirection = 'bottom'
- } else if (maxDistance === distanceToLeft) {
- this.fabDirection = 'left'
- } else {
- this.fabDirection = 'right'
- }
- },
-
- handleClick() {
- if (this.disabled) {
- return
- }
- if (!this.expandable) {
- this.$emit('click')
- return
- }
- this.isActive = !this.isActive
- this.$emit('change')
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import '../../libs/css/components.scss';
- .u-fab {
- position: fixed;
-
- &__trigger-wrapper {
- position: relative;
- z-index: 2;
- }
-
- &__actions {
- position: absolute;
- z-index: 1;
- display: flex;
- align-items: center;
- &--top {
- bottom: 100%;
- left: 50%;
- transform: translateX(-50%);
- margin-bottom: 8px;
- flex-direction: column-reverse;
- }
-
- &--right {
- left: 100%;
- top: 50%;
- transform: translateY(-50%);
- margin-left: 8px;
- flex-direction: row;
- }
-
- &--bottom {
- top: 100%;
- left: 50%;
- transform: translateX(-50%);
- margin-top: 8px;
- flex-direction: column;
- }
-
- &--left {
- right: 100%;
- top: 50%;
- transform: translateY(-50%);
- margin-right: 8px;
- flex-direction: row-reverse;
- }
-
- }
- }
- </style>
|