uniapp vue3 ts自定义底部 tabbar菜单

组件源码

复制代码
<script lang="ts" setup>
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import mIcon from '../m-icon/m-icon.vue';

interface TabItem {
  text: string;
  iconPath: string;
  selectedIconPath: string;
  count?: number;
  isDot?: boolean;
  midButton?: boolean;
  pagePath?: string;
  customIcon?: boolean;
}

const props = defineProps({
  show: { type: Boolean, default: true },
  modelValue: { type: [String, Number], default: 0 },
  bgColor: { type: String, default: '#ffffff' },
  height: { type: [String, Number], default: '50px' },
  iconSize: { type: [String, Number], default: 40 },
  midButtonSize: { type: [String, Number], default: 90 },
  activeColor: { type: String, default: '#303133' },
  inactiveColor: { type: String, default: '#606266' },
  midButton: { type: Boolean, default: false },
  list: { type: Array as () => TabItem[], default: () => [] },
  beforeSwitch: { type: Function as unknown as () => (index: number) => Promise<boolean> | boolean, default: null },
  borderTop: { type: Boolean, default: true },
  hideTabBar: { type: Boolean, default: true },
});

const emit = defineEmits(['change', 'update:modelValue']);
const midButtonLeft = ref<string>('50%');
const pageUrl = ref<string>('');

onMounted(() => {
  if (props.hideTabBar) uni.hideTabBar();

  const pages = getCurrentPages();
  pageUrl.value = pages[pages.length - 1]?.route || '';

  if (props.midButton) getMidButtonLeft();
});
const normalizePath = (path: string) => path.replace(/^\/+/, '').split('?')[0];

const elIconPath = (index: number) => {
   const item = props.list[index];
  const pagePath = item.pagePath;

  if (pagePath) {
    const current = normalizePath(pageUrl.value);
    const target = normalizePath(pagePath);
    return current === target ? item.selectedIconPath : item.iconPath;
  } else {
    return index === props.modelValue ? item.selectedIconPath : item.iconPath;
  }
};

const elColor = (index: number) => {
  const item = props.list[index];
  const pagePath = item.pagePath;
  if (pagePath) {
    return (pagePath === pageUrl.value || '/' + pagePath === pageUrl.value)
      ? props.activeColor
      : props.inactiveColor;
  } else {
    return index === props.modelValue ? props.activeColor : props.inactiveColor;
  }
};

const clickHandler = async (index: number) => {
  if (typeof props.beforeSwitch === 'function') {
    const result = props.beforeSwitch(index);
    if (result instanceof Promise) {
      try {
        const res = await result;
        if (res) switchTab(index);
      } catch {}
    } else if (result === true) {
      switchTab(index);
    }
  } else {
    switchTab(index);
  }
};

const switchTab = (index: number) => {
  emit('change', index);
  const item = props.list[index];
  if (item.pagePath) {
    uni.switchTab({ url: item.pagePath });
  } else {
    emit('update:modelValue', index);
  }
};

const getOffsetRight = (count?: number, isDot?: boolean) => {
  if (isDot) return -20;
  else if ((count || 0) > 9) return -40;
  else return -30;
};

const getMidButtonLeft = () => {
  const res = uni.getSystemInfoSync();
  midButtonLeft.value = res.windowWidth / 2 + 'px';
};
</script>

<template>
  <view v-show="show" class="u-tabbar" @touchmove.stop.prevent>
    <view
      class="u-tabbar__content safe-area-inset-bottom"
      :style="{
        height: height + (typeof height === 'number' ? 'rpx' : ''),
        backgroundColor: bgColor,
      }"
      :class="{ 'u-border-top': borderTop }"
    >
      <view
        v-for="(item, index) in list"
        :key="index"
        class="u-tabbar__content__item"
        :class="{ 'u-tabbar__content__circle': midButton && item.midButton }"
        @tap.stop="() => clickHandler(index)"
        :style="{ backgroundColor: bgColor }"
      >
        <view
          :class="[
            midButton && item.midButton
              ? 'u-tabbar__content__circle__button'
              : 'u-tabbar__content__item__button'
          ]"
        >
        <!-- {{ elIconPath(index) }} -->
          <m-images mode="aspectFit" :url="elIconPath(index)" width="48rpx" height="48rpx"></m-images>
          <!-- <m-icon
            :size="midButton && item.midButton ? midButtonSize : iconSize"
            :name="elIconPath(index)"
            img-mode="scaleToFill"
            :color="elColor(index)"
            :custom-prefix="item.customIcon ? 'custom-icon' : 'uicon'"
          /> -->
          <!-- <u-badge
            v-show="item.count || item.isDot"
            :count="item.count"
            :is-dot="item.isDot"
            :offset="[-2, getOffsetRight(item.count, item.isDot)]"
          /> -->
        </view>
        <view
          class="u-tabbar__content__item__text"
          :style="{ color: elColor(index) }"
        >
          <text class="u-line-1">{{ item.text }}</text>
        </view>
      </view>
      <view
        v-show="midButton"
        class="u-tabbar__content__circle__border"
        :class="{ 'u-border': borderTop }"
        :style="{ backgroundColor: bgColor, left: midButtonLeft }"
      />
    </view>
    <view
      class="u-fixed-placeholder safe-area-inset-bottom"
      :style="{ height: `calc(${height}${typeof height === 'number' ? 'rpx' : ''} + ${midButton ? 48 : 0}rpx)` }"
    />
  </view>
</template>

<style scoped lang="scss">
//@import "../../libs/css/style.components.scss";
@mixin vue-flex($direction: row) {
	/* #ifndef APP-NVUE */
	display: flex;
	flex-direction: $direction;
	/* #endif */
}
.u-fixed-placeholder {
  box-sizing: content-box;
}
.u-tabbar {
  
  &__content {
    padding-bottom: env(safe-area-inset-bottom);
  height: calc(50px + env(safe-area-inset-bottom));
    @include vue-flex;
    align-items: center;
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    z-index: 998;
    box-sizing: content-box;
    &__circle__border {
      border-radius: 100%;
      width: 110rpx;
      height: 110rpx;
      top: -48rpx;
      position: absolute;
      z-index: 4;
      background-color: #ffffff;
      left: 50%;
      transform: translateX(-50%);
      &:after {
        border-radius: 100px;
      }
    }
    &__item {
      flex: 1;
      justify-content: center;
      height: 100%;
      padding: 12rpx 0;
      @include vue-flex;
      flex-direction: column;
      align-items: center;
      position: relative;
      &__button {
        position: absolute;
        top: 14rpx;
        left: 50%;
        transform: translateX(-50%);
      }
      &__text {
        color: #ff7300;
        font-size: 26rpx;
        line-height: 28rpx;
        position: absolute;
        bottom: 14rpx;
        left: 50%;
        transform: translateX(-50%);
        width: 100%;
        text-align: center;
      }
    }
    &__circle {
      position: relative;
      @include vue-flex;
      flex-direction: column;
      justify-content: space-between;
      z-index: 10;
      height: calc(100% - 1px);
      &__button {
        width: 90rpx;
        height: 90rpx;
        border-radius: 100%;
        @include vue-flex;
        justify-content: center;
        align-items: center;
        position: absolute;
        background-color: #ffffff;
        top: -40rpx;
        left: 50%;
        z-index: 6;
        transform: translateX(-50%);
      }
    }
  }
}
</style>

说明

复制代码
let list = [
  {
    // 非凸起按钮未激活的图标,可以是uView内置图标名或自定义扩展图标库的图标
    // 或者png图标的【绝对路径】,建议尺寸为80px * 80px
    // 如果是中间凸起的按钮,只能使用图片,且建议为120px * 120px的png图片
    iconPath: "home",
    // 激活(选中)的图标,同上
    selectedIconPath: "home-fill",
    // 显示的提示文字
    text: "首页",
    // 红色角标显示的数字,如果需要移除角标,配置此参数为0即可
    count: 2,
    // 如果配置此值为true,那么角标将会以红点的形式显示
    isDot: true,
    // 如果使用自定义扩展的图标库字体,需配置此值为true
    // 自定义字体图标库教程
    customIcon: false,
    // 如果是凸起按钮项,需配置此值为true
    midButton: false,
    // 点击某一个item时,跳转的路径,此路径必须是pagees.json中tabBar字段中定义的路径
    pagePath: "", 路径需要以"/"开头
  },
];

代码

复制代码
 <view>
    <view class="u-page">
      <!-- 所有内容的容器 -->
    </view>
    <!-- 与包裹页面所有内容的元素u-page同级,且在它的下方 -->
    <m-tabbar v-model="current" :list="list" :mid-button="true"></m-tabbar>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        list: [
          {
            iconPath: "home",
            selectedIconPath: "home-fill",
            text: "首页",
            count: 2,
            isDot: true,
            customIcon: false,
          },
          {
            iconPath: "photo",
            selectedIconPath: "photo-fill",
            text: "放映厅",
            customIcon: false,
          },
          {
            iconPath: xxxx",
            selectedIconPath: "xxx",
            text: "发布",
            midButton: true,
            customIcon: false,
          },
          {
            iconPath: "play-right",
            selectedIconPath: "play-right-fill",
            text: "直播",
            customIcon: false,
          },
          {
            iconPath: "account",
            selectedIconPath: "account-fill",
            text: "我的",
            count: 23,
            isDot: false,
            customIcon: false,
          },
        ],
        current: 0,
      };
    },
  };
</script>
  • height配置导航栏高度,建议使用默认值即可,默认为50px,与 uni-app 自带系统导航栏高度一致

  • bg-color组件的背景颜色

  • active-color与inactive-color配置提示文字和图标的颜色(如果是字体图标的话),可以搭配bg-color达到自定义导航栏主题的效果

    自定义 tabbar 场景,我们不建议在一个页面内通过几个组件,用v-if切换去模拟各个页面,而应该使用 uni-app 自带的 tabbar 系统,同时隐藏原生的 tabbar, 再引入自定导航栏,这样可以保证原有性能,同时又能自定义 tabbar,思路如下:

    在 pages.json 中正常定义 tabbar 逻辑和字段,只需配置tabBar字段list中的pagePath(需以"/"开头)属性即可
    在各个 tabbar 页面引入u-tabbar组件,组件会默认自动通过uni.hideTabBar()隐藏系统 tabbar
    通过vuex引用同一份 tabbar 组件的list参数,这样可以做到修改某一个页面的u-tabbar数据,其他页面的u-tabbar也能同步更新
    组件内部会自动处理各种跳转的逻辑,同时需要注意以下两点:
    要在list参数中配置pagePath路径,此路径为pages.json中定义的 tabbar 字段的路径
    此种方式,无需通过v-model绑定活动项,内部会自动进行判断和跳转

底部菜单配置

复制代码
// 底部菜单
// import { useImageAssets } from '@/utils/useImageAssets';
// const images=useImageAssets()
// "/static/tabbar/home1.png"
const listTabbar =[{
            iconPath:"/static/images/tabbar/home.png",
            selectedIconPath: "/static/images/tabbar/home1.png",
            text: "首页",
            pagePath:"/pages/index/index",
            customIcon: false,
          },
          {
            iconPath: "/static/images/tabbar/class.png",
            selectedIconPath: "/static/images/tabbar/class1.png",
            text: "分类",
            customIcon: false,
              pagePath:"/pages/classList/index",
          },
          {
            iconPath: "/static/images/tabbar/shapping.png",
            selectedIconPath: "/static/images/tabbar/shapping1.png",
            text: "购物车",
            midButton: true,
              pagePath:"/pages/shapping/index",
            customIcon: false,
          },
          {
            iconPath: "/static/images/tabbar/fujin.png",
            selectedIconPath: "/static/images/tabbar/fujin1.png",
            text: "附近",
            customIcon: false,
              pagePath:"/pages/near/index",
          },
          {
            iconPath: "/static/images/tabbar/my.png",
            selectedIconPath: "/static/images/tabbar/my1.png",
            text: "我的",
            pagePath:"/pages/my/my",
            customIcon: false,
          },]
		  
		  
		  
export {
	listTabbar
}

注意

  • 小程序 "custom": true,添加这个

    uniap pages.json添加配置

    "tabBar": {
    "color": "#666666",
    "selectedColor": "#40AE36",
    "custom": true,
    "borderStyle": "white",
    "backgroundColor": "#ffffff",
    "list": [
    {
    "iconPath": "static/images/tabbar/home.png",
    "selectedIconPath": "static/images/tabbar/home1.png",
    "pagePath": "pages/index/index",
    "text": "首页"
    },
    {
    "iconPath": "static/images/tabbar/class.png",
    "selectedIconPath": "static/images/tabbar/class1.png",
    "pagePath": "pages/classList/index",
    "text": "分类"
    },
    {
    "iconPath": "static/images/tabbar/shapping.png",
    "selectedIconPath": "static/images/tabbar/shapping1.png",
    "pagePath": "pages/shapping/index",
    "text": "购物车"
    },
    {
    "iconPath": "static/images/tabbar/fujin.png",
    "selectedIconPath": "static/images/tabbar/fujin1.png",
    "pagePath": "pages/near/index",
    "text": "附近"
    },
    {
    "iconPath": "static/images/tabbar/my.png",
    "selectedIconPath": "static/images/tabbar/my1.png",
    "pagePath": "pages/my/my",
    "text": "我的"
    }
    ]
    }

app.vue中隐藏

这样自定义tabbar就可以使用uni.switchTab跳转了

复制代码
onLaunch(async () => {

  uni.hideTabBar()

});

使用

index.vue

复制代码
<m-tabbar v-model="current" :list="listTabbar" :show="true" :hideTabBar="true"></m-tabbar>

listTabbar 菜单配置
hideTabBar 是否隐藏 
current 当前的菜单


示例代码
<template>
    <view>
        <text>哈哈哈</text>
        <m-tabbar v-model="current" :list="listTabbar" :show="true" :hideTabBar="true"></m-tabbar>
    </view>
    </template>
<script lang="ts" setup>
import {listTabbar} from "@/config/tabbarConfig"
import { ref } from "vue";
const current = ref(1)
</script>
相关推荐
黑夜照亮前行的路10 分钟前
JavaScript 性能优化实战技术指南
javascript·性能优化
Stringzhua1 小时前
Vue数据的变更操作与表单数据的收集【6】
前端·javascript·vue.js
万少1 小时前
可可图片编辑 HarmonyOS 上架应用分享
前端·harmonyos
你的人类朋友2 小时前
git常见操作整理(持续更新)
前端·git·后端
无羡仙2 小时前
Webpack 核心实战:从零搭建支持热更新与 Babel 转译的现代前端环境
前端·webpack·前端框架
乐~~~2 小时前
el-date-picker type=daterange 日期范围限制
javascript·vue.js·elementui
你的人类朋友2 小时前
git中的Fast-Forward是什么?
前端·git·后端
JarvanMo3 小时前
天塌了?Flutter工程总监跑去苹果了?
前端
烛阴4 小时前
掌握 TypeScript 的边界:any, unknown, void, never 的正确用法与陷阱
前端·javascript·typescript