优秀的编程知识分享平台

网站首页 > 技术文章 正文

尝鲜 Vue3.0+Vite 自定义导航栏+弹窗组件

nanyue 2025-01-23 20:08:49 技术文章 6 ℃

前言

最近得空一直在捣鼓Vue3开发,想要快速上手,还是得写一些自定义组件。之前就有基于vue2.x写过一些自定义Navbar+Tabbar及弹窗组件。于是就撸起袖子开整。

今天主要是给大家分享一些如何快速上手Vue3开发。

目前Vue3的最新版本是V3.0.4star高达19.7K+。而且更新还很频繁。

# vue3官网
https://v3.vuejs.org/

# vue3中文网
https://v3.cn.vuejs.org/

# vue3仓库地址
https://github.com/vuejs/vue-next

# Vite构建工具
https://vite-design.surge.sh/

通过如下2种方法快速创建Vue3项目。

1、Vite构建工具

$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

如果安装了Vite脚手架 create-vite-app,也可以通过下面方法快速创建。

$ create-vite-app <project-name>

2、@vue/cli脚手架

通过CLI创建需要版本是V4.5以上。升级CLI,npm i @vue/cli -g

$ vue create <project-name>
# 选择 Vue3选项 即可
$ npm run serve

下面进入今天的主题,Vue3实现自定义导航栏+底部Tab+弹窗组件。

在components目录下新建headerBar.vue和tabBar.vue两个页面。

并新建一个components.js页面用于引入公共组件。

然后在main.js中注册即可。

1、自定义headerBar.vue

其实这个组件和vue2.x创建没啥区别。

<template>
    <div class="header-bar" :class="{'fixed': fixed, 'transparent fixed': transparent}">
        <div class="header-bar__wrap flexbox flex-alignc" :style="{'background': bgcolor, 'color': color, 'z-index': zIndex}">
            <!-- //返回 -->
            <div class="action hdbar-action__left" v-if="back && back!='false'" @click="$router.go(-1)">
                <slot name="backIco" /><slot name="backText" />
            </div>

            <!-- //标题 -->
            <div class="hdbar-title" :class="{'center': center}">
                <template v-if="$slots.title">
                    <slot name="title" />
                </template>
                <template v-else>
                    {{title}}
                </template>
            </div>

            <!-- //搜索框 -->
            <div class="action hdbar-action__search">
                <slot name="search" />
            </div>

            <!-- //右边按钮 -->
            <div class="action hdbar-action__right">
                <slot name="right" />
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        props: {
            // 是否返回
            back: { type: [Boolean, String], default: true },
            // 标题
            title: { type: String, default: '' },
            // 标题颜色
            color: { type: String, default: '#fff' },
            // 背景颜色
            bgcolor: { type: String, default: '#22d59c' },
            // 标题是否居中
            center: { type: [Boolean, String], default: false },
            // 是否固定
            fixed: { type: [Boolean, String], default: false },
            // 背景透明
            transparent: { type: [Boolean, String], default: false },
            // 设置层级
            zIndex: { type: [Number, String], default: '2020' },
        }
    }
</script>

支持自定义背景色、文字颜色、左侧图标、标题、搜索框,右侧支持图标|文字|图片

<header-bar :back="true" bgcolor="#f9f9f9" color="#ff0" center transparent>
  <template #backIco><i class="iconfont icon-close"></i></template>
  <template v-slot:backText>返回</template>
  <!--标题-->
  <template v-slot:title>
    <img src="~/assets/img/logo.png" height="16" /> <em>Vue3.0</em>
  </template>
  <!--右侧按钮-->
  <template #right><i class="iconfont icon-search"></i></template>
</header-bar>

2、自定义tabBar.vue

这里的逻辑代码主要是在setup里处理。

<template>
    <div class="tab-bar" :class="{'fixed': fixed}">
        <div class="tab-bar__wrap flexbox flex-alignc" :style="{'background': bgcolor}">
            <div v-for="(item, index) in tabs" :key="index" class="navigator" :class="currentTabIndex == index ? 'on' : ''" @click="switchTabs(index, item)">
                <div class="ico" :class="{'dock': item.dock}">
                    <i v-if="item.dock" class="dock-bg" :style="{'background': item.dockBg ? item.dockBg : activeColor}"></i>
                    <i v-if="item.icon" class="iconfont" :class="item.icon" :style="{'color': (currentTabIndex == index && !item.dock ? activeColor : color), 'font-size': item.iconSize}"></i>
                    <img v-if="item.img" class="iconimg" :src="currentTabIndex == index && !item.dock ? item.activeImg : item.img" :style="{'font-size': item.iconSize}" />
                    <em v-if="item.badge" class="nuxt__badge">{{item.badge}}</em>
                    <em v-if="item.dot" class="nuxt__badge-dot"></em>
                </div>
                <div class="txt" :style="{'color': (currentTabIndex == index ? activeColor: color)}">{{item.title}}</div>
            </div>
        </div>
    </div>
</template>

<script>
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
    export default {
        props: {
            current: { type: [Number, String], default: 0 },
            // 背景色
            bgcolor: { type: String, default: '#fff' },
            // 颜色
            color: { type: String, default: '#999' },
            // 激活颜色
            activeColor: { type: String, default: '#22d59c' },
            // 是否固定
            fixed: { type: [Boolean, String], default: false },
            // tab选项
            tabs: {
                type: Array,
                default: () => null
            },
        },
        emits: {
            click: null
        },
        setup(props, context) {
            const currentTabIndex = ref(props.current)
            const router = useRouter()
            const route = useRoute()

            // 匹配当前路由页面
            const _curPath = route.path
            props.tabs.map((item, index) => {
                if(item.path == _curPath) {
                    currentTabIndex.value = index
                }
            })

            const switchTabs = (index, item) => {
                currentTabIndex.value = index
                context.emit('click', index)
                if(item.path) {
                    router.push(item.path)
                }
            }
            return {
                currentTabIndex,
                switchTabs,
            }
        }
    }
</script>

支持自定义背景色、文字颜色|选中颜色、是否固定、点击选项(返回索引值)

另外还支持dock效果,图标支持iconfont及图片。

<tab-bar bgcolor="#f9f9f9" color="#f00" @click="tabbarClicked" :tabs="[
  {
    icon: 'icon-home', title: '首页', path: '/index',
  },
  {
    icon: 'icon-guanli', title: '管理', path: '/manage'
    badge: 1
  },
  {
    icon: 'icon-fabu', title: '发布',
    dock: true, dockBg: '#f60',
    iconSize: '20px',
  },
  {
    icon: 'icon-guanli', title: '联系人',
    img: 'http://www.xxx.com/contact.jpg',
    activeImg: 'http://www.xxx.com/contact_on.jpg',
  },
  {
    icon: 'icon-me', title: '我的',
    dot: true
  }]"
/>

// 点击事件
tabbarClicked(index) {
  console.log('tabbar索引值:' + index)
}

3、自定义V3Popup组件

V3Popup 基于Vue3开发的一款集合Alert、Dialog、ActionSheet、Toast等功能的移动端Vue3.0弹出框组件。

支持标签式+函数式两种调用方式。

// 标签式调用
<v3-popup
  v-model="showAlert"
  title="标题" 
  content="弹窗内容信息"
  type="ios"
  shadeClose="false"
  xclose
  z-index="2021"
  :btns="[
    {...},
    {...},
  ]"
/>
// 函数式调用
let $el = this.$v3popup({
  title: '标题',
  content: '弹窗内容信息',
  type: 'ios',
  shadeClose: false,
  xclose: true,
  zIndex: 2021,
  btns: [
    {text: '关闭'},
    {
      text: '确定',
      style: 'color:#09f;',
      click: () => {
        $el.close()
      }
    },
  ]
});

template模板写法和vue2.x没什么区别,主要是逻辑处理部分,既可以使用vue2.x的写法,也可以使用vue3的Composition API写法。

下面主要是使用Vue3写法实现逻辑。

<script>
    import { ref, reactive, watch, toRefs, nextTick, onMounted } from 'vue'
    /**
     * Vue3.0写法
     */
    let $index = 0, $locknum = 0, $timer = {}
    export default {
        props: {
            // 接收父组件v-model值,如果v-model:xxx,则这里需写xxx: {...}
            modelValue: { type: Boolean, default: false },
            // 弹框标识符,相同ID共享一个实例
            id: {
                type: String, default: ''
            },
            title: String,
            content: String,
            type: String,
            popupStyle: String,
            icon: String,
            shade: { type: [Boolean, String], default: true },
            shadeClose: { type: [Boolean, String], default: true },
            opacity: { type: [Number, String], default: '' },
            round: Boolean,
            xclose: Boolean,
            xposition: { type: String, default: 'right' },
            xcolor: { type: String, default: '#333' },
            anim: { type: String, default: 'scaleIn' },
            position: String,
            follow: { type: Array, default: null },
            time: { type: [Number, String], default: 0 },
            zIndex: { type: [Number, String], default: '8080' },
            teleport: [String, Object],
            btns: {
                type: Array, default: null
            },
            onSuccess: { type: Function, default: null },
            onEnd: { type: Function, default: null },

            // 接收函数式移除指令
            remove: Function,
        },
        emits: [
            'update:modelValue',
        ],
        setup(props, context) {
            const elRef = ref(null)

            const data = reactive({
                opened: false,
                closeCls: '',
                toastIcon: {
                    //...
                }
            })

            watch(() => props.modelValue, (val) => {
                if(val) {
                    open()
                } else {
                    close()
                }
            })

            onMounted(() => {
                if(props.modelValue) {
                    open()
                }
            })

            // 打开弹框
            const open = () => {
                if(data.opened) return
                data.opened = true
                typeof props.onSuccess === 'function' && props.onSuccess()

                const dom = elRef.value
                dom.style.zIndex = getZIndex() + 1

                if(JSON.parse(props.shade)) {
                    if(!document.body.classList.contains('vui__body-hidden')) {
                        document.body.classList.add('vui__body-hidden')
                    }
                    $locknum++
                }

                // 倒计时关闭
                if(props.time) {
                    $index++
                    // 避免重复点击
                    if($timer[$index] !== null) clearTimeout($timer[$index])
                    $timer[$index] = setTimeout(() => {
                        close()
                    }, parseInt(props.time) * 1000)
                }
                //...
            }

            // 关闭弹框
            const close = () => {
                data.closeCls = true
                setTimeout(() => {
                    data.opened = false
                    data.closeCls = false
                    if(JSON.parse(props.shade)) {
                        $locknum--
                        if(!$locknum) {
                            document.body.classList.remove('vui__body-hidden')
                        }
                    }
                    if(props.time) {
                        $index--
                    }
                    context.emit('update:modelValue', false)
                    typeof props.onEnd === 'function' && props.onEnd()
                }, 200)
            }

            // 点击遮罩层
            const shadeClicked = () => {
                if(JSON.parse(props.shadeClose)) {
                    close()
                }
            }
            // 按钮事件
            const btnClicked = (e, index) => {
                //...
            }
            // 获取弹窗层级
            const getZIndex = () => {
                //...
            }
            // 获取弹窗坐标点
            const getPos = (x, y, ow, oh, winW, winH) => {
                let l = (x + ow) > winW ? x - ow : x;
                let t = (y + oh) > winH ? y - oh : y;
                return [l, t];
            }

            return {
                ...toRefs(data),
                shadeClicked,
                btnClicked,
                close,
                elRef,
            }
        }
    }
</script>

vue3中watch监听事件,可以监听一个、多个值(对象/数组)。

<script>
    import { ref, computed, reactive, toRefs, watch, nextTick } from 'vue'
    export default {
        setup() {
            const count = ref(0)
            const data = reactive({
                num: 0,
                total: computed(() => data.num * 2),
                handlePlus: () => {
                    data.num++
                }
            })

            // 监听器watch使用
            // 1、监听ref一个值count
            // watch(count, () => {})
            watch(count, (newVal, oldVal) => {
                console.log('count updated:' + count.value)
                console.log(newVal, oldVal, "--新旧值");
            })
            // 监听reactive中某一项
            watch(() => data.num, (newVal, oldVal) => {
                console.log('+++count updated:' + data.num)
                console.log(newVal, oldVal, "==新旧值");
            })
            // 2、监听整个reactive中data对象
            watch(data, () => {
                console.log('==count updated:' + data.num)
            })
            // 3、监听多个值
            // watch([count, data], () => {})
            watch([count, () => data.num], () => {
                console.log('某一项updated:' + count.value + '---' + data.num)
            })

            return {
                //...
            }
        }
    }
</script>

注意:Vue3不支持div的slot插槽。

// 不支持
<div slot="content"></div>

// 支持
<template v-slot:content>显示自定义插槽内容!</template>
<template #content>显示自定义插槽内容!</template>

ok,今天就分享到这里。篇幅有些长,感谢大家的阅读。

Tags:

最近发表
标签列表