UniApp 制作悬浮按钮与菜单组件
在移动应用开发中,悬浮按钮(Floating Action Button, FAB)和悬浮菜单是提升操作便捷性和界面交互感的重要控件。无论是社交、工具、内容创作还是管理后台,悬浮按钮都能为用户提供快速入口,极大提升操作效率。随着 HarmonyOS(鸿蒙)生态的不断壮大,开发一套兼容鸿蒙的悬浮按钮与菜单组件变得尤为重要。本文将结合 UniApp 跨平台开发的优势,详细讲解如何实现一个高效、易扩展、适配鸿蒙的悬浮按钮与菜单组件,并分享实际案例和鸿蒙适配经验。
为什么要自定义悬浮按钮与菜单?
虽然 UniApp 提供了基础的按钮组件,但在实际项目中,往往会遇到如下需求:
- 支持自定义图标、颜色、阴影、动画等;
- 支持展开多个操作菜单,支持自定义菜单项;
- 兼容多端,尤其是 HarmonyOS 设备的适配和体验优化;
- 支持拖拽、吸边、自动隐藏等高级交互。
自定义悬浮按钮与菜单不仅能满足个性化需求,还能提升整体产品体验和品牌一致性。
组件设计思路
设计一个悬浮按钮与菜单组件,需要考虑以下几个方面:
- 位置与布局:支持右下、左下、右上、左上等多种悬浮位置。
- 交互体验:支持点击展开/收起菜单,动画过渡,菜单项点击反馈。
- 样式定制:支持自定义颜色、图标、阴影、圆角、大小等。
- 鸿蒙适配:在鸿蒙端保证动画、手势、性能等能力正常。
- 易用性与扩展性:props 设计合理,便于业务集成和后续扩展。
组件实现
我们以一个通用的 FloatingMenu 组件为例,支持主按钮+多个菜单项,带动画。
1. 组件结构
在 components/floating-menu/floating-menu.vue
下新建组件:
<template>
<view class="fab-wrapper" :style="wrapperStyle">
<view
v-for="(item, idx) in menuItems"
:key="item.key || idx"
class="fab-menu-item"
:style="menuItemStyle(idx)"
@click="onMenuClick(item, idx)"
v-show="showMenu"
>
<text :class="['fab-menu-icon', item.icon]">{{ item.iconText }}</text>
<text class="fab-menu-label">{{ item.label }}</text>
</view>
<view class="fab-btn" :style="btnStyle" @click="toggleMenu">
<text :class="['fab-icon', icon]">{{ iconText }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'FloatingMenu',
props: {
icon: {
type: String,
default: '+'
},
iconText: {
type: String,
default: ''
},
color: {
type: String,
default: '#007dff'
},
position: {
type: String,
default: 'right-bottom' // right-bottom, left-bottom, right-top, left-top
},
menuItems: {
type: Array,
default: () => []
},
menuDirection: {
type: String,
default: 'up' // up, down, left, right
},
size: {
type: String,
default: '96rpx'
},
shadow: {
type: Boolean,
default: true
}
},
data() {
return {
showMenu: false
};
},
computed: {
wrapperStyle() {
let style = {
position: 'fixed',
zIndex: 9999
};
if (this.position.includes('bottom')) style.bottom = '60rpx';
if (this.position.includes('top')) style.top = '60rpx';
if (this.position.includes('right')) style.right = '40rpx';
if (this.position.includes('left')) style.left = '40rpx';
return style;
},
btnStyle() {
return {
width: this.size,
height: this.size,
background: this.color,
borderRadius: '50%',
boxShadow: this.shadow ? '0 8rpx 32rpx rgba(0,125,255,0.18)' : 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '48rpx',
transition: 'background 0.2s'
};
}
},
methods: {
toggleMenu() {
this.showMenu = !this.showMenu;
},
menuItemStyle(idx) {
const gap = 120;
let style = {
position: 'absolute',
opacity: this.showMenu ? 1 : 0,
transition: 'all 0.3s',
zIndex: 9998,
right: this.position.includes('right') ? '0' : 'auto',
left: this.position.includes('left') ? '0' : 'auto',
bottom: this.position.includes('bottom') ? '0' : 'auto',
top: this.position.includes('top') ? '0' : 'auto',
};
if (this.menuDirection === 'up') {
style.transform = this.showMenu
? `translateY(-${(idx + 1) * gap}rpx)`
: 'translateY(0)';
} else if (this.menuDirection === 'down') {
style.transform = this.showMenu
? `translateY(${(idx + 1) * gap}rpx)`
: 'translateY(0)';
} else if (this.menuDirection === 'left') {
style.transform = this.showMenu
? `translateX(-${(idx + 1) * gap}rpx)`
: 'translateX(0)';
} else if (this.menuDirection === 'right') {
style.transform = this.showMenu
? `translateX(${(idx + 1) * gap}rpx)`
: 'translateX(0)';
}
style.width = this.size;
style.height = this.size;
style.background = this.color;
style.borderRadius = '50%';
style.display = 'flex';
style.alignItems = 'center';
style.justifyContent = 'center';
style.color = '#fff';
style.fontSize = '40rpx';
return style;
},
onMenuClick(item, idx) {
this.$emit('select', { item, idx });
this.showMenu = false;
}
}
};
</script>
<style scoped>
.fab-wrapper {
/* 通过js动态设置fixed位置 */
}
.fab-btn {
box-shadow: 0 8rpx 32rpx rgba(0,125,255,0.18);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.fab-btn:active {
background: #005bb5;
}
.fab-icon {
font-size: 48rpx;
color: #fff;
}
.fab-menu-item {
box-shadow: 0 8rpx 32rpx rgba(0,125,255,0.12);
cursor: pointer;
user-select: none;
transition: all 0.3s;
position: absolute;
}
.fab-menu-label {
display: none;
}
</style>
2. 组件使用与页面集成
在页面中引用并使用 FloatingMenu 组件,实现悬浮按钮与菜单:
<template>
<view class="demo-fab">
<floating-menu
:menu-items="menuList"
icon="+"
color="#ff4d4f"
position="right-bottom"
menu-direction="up"
@select="onMenuSelect"
/>
<view class="content">页面内容区域...</view>
</view>
</template>
<script>
import FloatingMenu from '@/components/floating-menu/floating-menu.vue';
export default {
components: { FloatingMenu },
data() {
return {
menuList: [
{ label: '新建', icon: '', iconText: '📝', key: 'new' },
{ label: '分享', icon: '', iconText: '🔗', key: 'share' },
{ label: '设置', icon: '', iconText: '⚙️', key: 'setting' }
]
};
},
methods: {
onMenuSelect({ item, idx }) {
uni.showToast({ title: `点击了:${item.label}`, icon: 'none' });
}
}
};
</script>
<style scoped>
.demo-fab {
min-height: 100vh;
background: #f8f8f8;
position: relative;
}
.content {
padding: 40rpx;
font-size: 32rpx;
color: #333;
}
</style>
3. HarmonyOS 适配与优化建议
- 动画体验:鸿蒙端对 CSS 动画、transform 支持良好,建议多端真机测试。
- 手势交互:如需支持拖拽、吸边,可结合 touch 事件和动画实现。
- 菜单扩展:可支持菜单项带文字、图标、徽标等丰富内容。
- UI 细节:鸿蒙设备分辨率多样,建议用
vw
/rpx
单位自适应。 - 无障碍支持:为按钮和菜单项添加 aria-label,提升可访问性。
4. 实际案例与体验优化
在某鸿蒙快应用项目中,悬浮按钮与菜单广泛应用于内容创作、工具栏、快捷操作等场景,结合动画和手势极大提升了用户体验。实际开发中还可结合以下优化:
- 支持长按、拖拽、吸边、自动隐藏等高级交互;
- 菜单项支持动态生成、权限控制;
- 支持多层级菜单、弹窗等复杂场景;
- 结合全局状态管理,菜单操作同步多模块数据。
总结
基于 UniApp 的悬浮按钮与菜单组件方案,既能兼容 HarmonyOS 生态,也能满足多端统一开发需求。通过灵活的样式定制、动画优化和交互扩展,可以为用户带来高效、友好的操作体验。希望本文能为你的鸿蒙/UniApp 项目提供实用参考。
如有问题或更好的实现思路,欢迎留言交流!