uniapp导航栏组件简单封装

小程序中,自定义导航栏组件是一个常见且实用的需求,它可以帮助开发者在不同平台上实现统一的导航栏样式,并且灵活适配各种屏幕尺寸和设备。

一、好处

  1. 用户体验:保持页面一致性,包括颜色、字体和布局等方面,提升用户体验。
  2. 适应性和灵活性:便于调整和优化。可以添加特定页面所需的功能按钮、搜索栏等。
  3. 提高开发效率:可以重复使用,减少代码冗余,提高开发效率。
  4. 功能扩展和定制化:功能扩展,如下拉菜单、动态标题等,满足不同页面和用户需求。

二、思路

  1. 隐藏自带的导航栏。
  2. 创建自定义导航栏组件。
  3. 导航栏组件使用。

三、实现

  1. pages.json 文件中设置 "navigationStyle": "custom"
pages.json 复制代码
{
    "pages": [
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "\u200e",
                "navigationStyle": "custom"
            }
        }
    ]
    ...
}

不需要自定义导航栏且不需要navigationBarTitleText,在浏览器上会显示当前页面url,可以设置"navigationBarTitleText": "\u200e"

  1. 创建自定义导航栏组件
    可以在uniApp项目的components目录下新建一个custom-navbar文件夹,并在其中创建custom-navbar.vue文件。小程序端根据状态栏、胶囊计算导航栏width、height。
vue 复制代码
<template>
    <view class="">
        <view
            class="navbar-box border-box"
            :style="[navbarStyle]"
            :class="[
                {
                    'border-bottom': borderBottom,
                    'navbar-box-image': bgImage,
                    'navbar-fixed': isFixed,
                },
            ]">
            <view class="status-bar" :style="[{ height: statusBarHeight + 'px' }]"></view>
            <view class="navbar-inner" :style="[navbarInnerStyle]">
                <slot name="back">
                    <view
                        class="flex align-center navbar-inner-back-wrap"
                        :class="[{ 'navbar-inner-back-border': showLine }]">
                        <template v-if="isBack">
                            <view
                                class="navbar-inner-back text-nowrap"
                                :style="[
                                    {
                                        color: backIconColor,
                                        fontSize: backIconSize + 'rpx',
                                    },
                                ]"
                                @tap="back()">
                                <text class="iconfont icon-xiangzuo"> </text>
                                <text v-if="backText" class="back-text line-1" :style="[backTextStyle]">
                                    {{ backText }}
                                </text>
                            </view>
                        </template>
                        <view class="line" v-if="isBack && isHome" :style="[{ background: backIconColor }]"></view>

                        <template v-if="isHome">
                            <view
                                class="home-btn"
                                :style="[
                                    {
                                        color: backIconColor,
                                        fontSize: backIconSize + 'rpx',
                                    },
                                ]"
                                @tap="home()">
                                <text class="iconfont icon-shouye"></text>
                            </view>
                        </template>
                        <view
                            class="line"
                            v-if="(isBack || isHome) && menu && menu.length"
                            :style="[{ background: backIconColor }]"></view>
                        <template v-if="menu && menu.length">
                            <view
                                class="home-btn menu-btn"
                                :style="[
                                    {
                                        color: backIconColor,
                                        fontSize: backIconSize + 'rpx',
                                    },
                                ]"
                                @tap="showMenu()">
                                <text class="iconfont icon-caidan"></text>
                                <view v-show="show">
                                    <view class="menu-mask"></view>
                                    <view class="menu-wrap">
                                        <view class="arrow"></view>
                                        <view
                                            v-for="(item, index) in menu"
                                            :key="index"
                                            class="item text-wrap"
                                            :class="[
                                                {
                                                    'item-disabled': item.disabled,
                                                },
                                            ]"
                                            @tap="menuClick(item, index)">
                                            <text>{{ item.title }}</text>
                                        </view>
                                    </view>
                                </view>
                            </view>
                        </template>
                        </view>
                </slot>
            <slot name="content">
                <view class="navbar-inner-content flex-1" v-if="title">
                    <view class="line-1" :style="[navTitleStyle]">
                        {{ title }}
                    </view>
                </view>
            </slot>
        </view>
    </view>
    <!-- 解决fixed定位后导航栏塌陷的问题 -->
    <view
        class="navbar-placeholder"
        v-if="isFixed && !immersive"
        :style="[
            {
                width: '100%',
                height: navHeight + statusBarHeight + 'px',
            },
        ]"></view>
</view>
</template>

<script>
let { windowWidth, statusBarHeight } = uni.getSystemInfoSync();
let top = 0,
	left = 12,
	right = 12,
	bottom = 0,
	width = windowWidth,
	height = 44;
// 小程序端自定义导航,根据状态栏,胶囊计算导航栏width、height、left、right、top、bottom
// #ifdef MP
let menuButtonInfo = uni.getMenuButtonBoundingClientRect();
top = menuButtonInfo.top - (menuButtonInfo.top - statusBarHeight);
bottom =
	top + (menuButtonInfo.top - statusBarHeight) * 2 + menuButtonInfo.height;
left = windowWidth - menuButtonInfo.right;
right = menuButtonInfo.width + left * 2;
width = windowWidth - left * 2 - menuButtonInfo.width - left;
height = bottom - top;
// #endif

export default {
    props: {
        // 是否显示返回
        isBack: {
            type: Boolean,
            default: true,
        },
        // 返回箭头的颜色
        backIconColor: {
            type: String,
            default: "#606266",
        },
        // 左边返回图标的大小,rpx
        backIconSize: {
            type: [String, Number],
            default: "40",
        },
        // 返回的文字提示
        backText: {
            type: String,
            default: "",
        },
        // 返回的文字的样式
        backTextStyle: {
            type: Object,
            default() {
                return {
                    color: "#606266",
                };
            },
        },
        // 是否显示回到主页
        isHome: {
            type: Boolean,
            default: false,
        },
        // 导航栏标题
        title: {
            type: String,
            default: "",
        },
        // 标题的颜色
        titleColor: {
            type: String,
            default: "#606266",
        },
        // 标题字体是否加粗
        titleBold: {
            type: Boolean,
            default: false,
        },
        // 标题的字体大小
        titleSize: {
            type: [String, Number],
            default: 32,
        },
        // 对象形式,因为用户可能定义一个纯色,或者线性渐变的颜色
        background: {
            type: Object,
            default() {
                return {
                    background: "#ffffff",
                };
            },
        },
        // 导航栏是否固定在顶部
        isFixed: {
            type: Boolean,
            default: true,
        },
        // 是否沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效
        immersive: {
            type: Boolean,
            default: false,
        },
        // 是否显示导航栏的下边框
        borderBottom: {
            type: Boolean,
            default: true,
        },
        zIndex: {
            type: [String, Number],
            default: "",
        },
        // 自定义返回
        isCustomBack: {
            type: Boolean,
            default: false,
        },
        // 背景图
        bgImage: {
            type: String,
            default: "",
        },
        // 菜单
        menu: {
            type: Array,
            dafault: [],
        },
    },
    emits: ["navbarHeight", "customBack", "homeClick", "menuClick"],
    data() {
        return {
            navWidth: width,
            navHeight: height,
            navTop: top,
            navLeft: left,
            navRight: right,
            statusBarHeight,
            // 返回rect
            backRect: {},
            // menu菜单显隐
            show: false,
        };
    },
    computed: {
        navbarStyle() {
            let style = {};
            style.zIndex = this.zIndex ? this.zIndex : "10";
            if(!this.bgImage) {
                Object.assign(style, this.background);
            }

            if (this.bgImage) {
                style.backgroundImage = `url(${this.bgImage})`;
            }
            return style;
        },
        navbarInnerStyle() {
            let style = {
                height: this.navHeight + "px",
                paddingLeft: this.navLeft + "px",
                paddingRight: this.navRight + "px",
            };
            return style;
        },
        // 计算title的margin-left值,保证title居中显示
        navTitleStyle() {
            let style = {
                color: this.titleColor,
                fontSize: this.titleSize + "rpx",
                fontWeight: this.titleBold ? "bold" : "normal",
            };
            let marginLeft = this.navRight - this.navLeft;
            if (this.backRect.width) {
                let backMarginLeft = uni.upx2px(20)
                marginLeft = marginLeft - this.backRect.width - backMarginLeft;
            }
            style.marginLeft = marginLeft + "px";
            return style;
        },
        showLine() {
            return (
                (this.isBack && this.isHome) || ((this.isBack || this.isHome) && this.menu && this.menu.length)
            );
        },
    },
    mounted() {
        // 导航栏的高度,包含状态栏的高度
        this.$emit("navbarHeight", this.navHeight + this.statusBarHeight);
        if (this.isBack) {
            this.$nextTick(async () => {
                this.backRect = await this.getRect(".navbar-inner-back-wrap");
            });
        }
    },
    methods: {
        // 查询节点信息
        getRect(selector, all) {
            return new Promise((resolve) => {
                uni.createSelectorQuery().in(this)[all ? "selectAll" : "select"](selector)
                    .boundingClientRect((rect) => {
                        if (all && Array.isArray(rect) && rect.length) {
                            resolve(rect);
                        }
                        if (!all && rect) {
                            resolve(rect);
                        }
                    })
                    .exec();
            });
        },
        // 返回
        back() {
            if (this.isCustomBack) {
                this.$emit("customBack");
            } else {
                let pages = getCurrentPages();
                pages.length > 1 && uni.navigateBack();
            }
        },
        home() {
            this.$emit("homeClick");
        },
        showMenu() {
            this.show = !this.show;
        },
        // 菜单点击
        menuClick(item, index) {
            if (item.disabled) return;
            this.$emit("menuClick", { ...item, index });
        },
    },
};
</script>

<style scoped lang="scss">
.border-box {
    box-sizing: border-box;
}
.text-nowrap {
    white-space: nowrap;
}
.text-wrap {
    white-space: pre-wrap;
    word-break: break-all;
    word-wrap: break-word;
}
.line-1 {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
.navbar-box {
    &.border-bottom {
        border-bottom: 1rpx solid #f0f0f0;
    }
    &.navbar-box-image {
        background-position: 0 0;
        background-repeat: no-repeat;
        background-size: cover;
    }
    &.navbar-fixed {
        position: fixed;
        top: 0;
        right: 0;
        left: 0;
    }
    .navbar-inner {
        display: flex;
        align-items: center;
        box-sizing: border-box;
        .navbar-inner-back-wrap {
            margin-right: 20rpx;
            .navbar-inner-back {
                .back-text {
                    margin-left: 10rpx;
                }
            }
            &.navbar-inner-back-border {
                padding: 0 20rpx;
                border: 1rpx solid #f0f0f0;
                border-radius: 40rpx;
                box-sizing: border-box;
            }
            .line {
                height: 32rpx;
                width: 1rpx;
                background-color: #f0f0f0;
                margin: 0 20rpx;
            }
            .menu-btn {
                position: relative;
            }
        }

        .navbar-inner-content {
            text-align: center;
        }
    }
}
$bgColor: #fff;
$borderColor: rgba(255, 255, 255, 0.4);
.menu-mask {
    position: fixed;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
}
.menu-wrap {
    box-sizing: border-box;
    position: absolute;
    top: calc(100% + 24rpx);
    left: 0;
    background-color: $bgColor;
    border: 1rpx solid $borderColor;
    border-radius: 12rpx;
    box-shadow: 0 4rpx 24rpx 0 rgba(0, 0, 0, 0.1);
    z-index: 3;
    padding: 8rpx 0;
    .arrow {
        position: absolute;
        display: block;
        width: 0;
        height: 0;
        border-color: transparent;
        border-style: solid;
        border-width: 12rpx;
        filter: drop-shadow(0 4rpx 24rpx rgba(0, 0, 0, 0.03));
        top: -12rpx;
        left: 10%;
        margin-right: 6rpx;
        border-top-width: 0;
        border-bottom-color: $borderColor;
        &::after {
            position: absolute;
            display: block;
            width: 0;
            height: 0;
            border-color: transparent;
            border-style: solid;
            border-width: 12rpx;
            content: " ";
            top: 2rpx;
            margin-left: -12rpx;
            border-top-width: 0;
            border-bottom-color: $bgColor;
        }
    }
    .item {
        min-width: 124rpx;
        max-width: 260rpx;
        font-size: 28rpx;
        padding: 10rpx 20rpx;
        box-sizing: border-box;
        color: #333;
        &.item-disabled {
            opacity: 0.4;
        }
    }
}
</style>

四、使用

使用示例

index.vue 复制代码
<template>
    <view>
        <custom-navbar backIconColor="#fff" titleColor="#fff" title="首页" :background="{background: 'linear-gradient(144deg,#af40ff,#4f46e5)'}"></custom-navbar>
        <view class="">
            页面
        </view>
    </view>
</template>

<script>
import customNavbar from "@/components/custom-navbar/custom-navbar.vue";
export default {
    component: {
        customNavbar,
    },
    data() {
        return {};
    },
    onLoad() {},
    methods: {},
};
</script>

渐变背景

index.vue 复制代码
<custom-navbar backIconColor="#fff" titleColor="#fff" title="首页" :background="{background: 'linear-gradient(144deg,#af40ff,#4f46e5)'}"></custom-navbar>

图片背景

index.vue 复制代码
<custom-navbar backIconColor="#fff" :isHome="true" titleColor="#fff" title="首页" bgImage="https://img1.baidu.com/it/u=3915920165,3171018890&fm=253&fmt=auto&app=120&f=JPEG"></custom-navbar>

下拉菜单

index.vue 复制代码
<custom-navbar backIconColor="#fff" titleColor="#fff" title="首页" :menu="[{title: '菜单一'}]" :background="{background: '#00AF57'}"></custom-navbar>

插槽,搜索框

index.vue 复制代码
<custom-navbar>
    <template v-slot:content>
        <view class="search-box">
            <text class="iconfont icon-sousuo ml-20" ></text>
            <text>搜索</text>
        </view>
    </template>
</custom-navbar>


<style scoped lang="scss">
.search-box {
    height: 80%;
    width: 100%;
    border-radius: 40rpx;
    background-color: #f0f0f0;
    padding: 0 24rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
}
.ml-20 {
    margin-right: 20rpx;
}
</style>

五、效果

注:uniapp,vue3

相关推荐
明似水1 小时前
Flutter 开发入门:从一个简单的计数器应用开始
前端·javascript·flutter
沐土Arvin1 小时前
前端图片上传组件实战:从动态销毁Input到全屏预览的全功能实现
开发语言·前端·javascript
爱编程的鱼2 小时前
C#接口(Interface)全方位讲解:定义、特性、应用与实践
java·前端·c#
sunbyte2 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | 页面布局 与 Vue Router 路由配置
前端·javascript·vue.js·tailwindcss
saadiya~2 小时前
Vue 3 实现后端 Excel 文件流导出功能(Blob 下载详解)
前端·vue.js·excel
摇摇奶昔x3 小时前
webpack 学习
前端·学习·webpack
阿珊和她的猫3 小时前
Vue Router中的路由嵌套:主子路由
前端·javascript·vue.js
_龙小鱼_4 小时前
Kotlin 作用域函数(let、run、with、apply、also)对比
java·前端·kotlin
霸王蟹4 小时前
React 19中如何向Vue那样自定义状态和方法暴露给父组件。
前端·javascript·学习·react.js·typescript