一、背景
uniapp项目中每个页面顶部都要加上公告滚动条,如下图:
实现方案:
- H5:如果路由使用
VueRouter
可以直接在APP.vue
组件里直接添加公告滚动条组件;如果没有使用VueRouter
则在页面挂载后使用insetBefore()
方法插入组件。 - 小程序:由于小程序没有开放根标签,没有办法在根标签下追加全局标签,所以需要在编译时加入。
二、实现
1、创建公告滚动条组件
/components/NoticeBar.vue
:
(以下为示例,具体的滚动条组件看文末)
<template>
<view>
NoticeBar
</view>
</template>
2、将设置为全局组件
在main.ts
中将组件挂载为全局组件,这样使用的时候就不用引入啦~:
import NoticeBar from '@/components/NoticeBar.vue'
Vue.component('NoticeBar', NoticeBar)
3、实现H5的全局公告滚动条
(1)有使用VueRouter
的方法:
原本是使用在App.vue
中使用NoticeBar
组件并引入页面组件<router-view>
的方法,虽然组件和页面都能正常显示,但是我们的uniapp项目中没有使用VueRouter
,所以页面回退的时候会有问题,所有回退都会跳转到首页。
在App.vue
中:
<template>
<view v-if="isH5">
<NoticeBar />
<router-view></router-view>
</view>
</template>
(2)没有使用VueRouter
的方法:
后面改成了最原始的方法:在页面挂载后使用insetBefore()
方法插入组件。
在main.ts
中:
// h5所有页面挂上noticeBar组件
const initNoticeBar = () => {
const app = document.getElementsByTagName('uni-app')['0']
const page = document.getElementsByTagName('uni-page')['0']
console.log('获取app和page', document, app, page, location)
app.insertBefore(new noticeBar().$mount().$el, page)
}
...
mountApp()
// #ifdef H5
initNoticeBar()
// #endif
4、实现小程序的全局公告滚动条
小程序的相对麻烦一些,因为小程序没有根组件,所以需要每个页面都去插入公告滚动条组件,由于我们小程序有几十个页面,一个个页面去加累死人,可以在编译时加入遍历每个页面template,将组件插入第一个元素里。具体做法如下:
在vue.config.js
中加入如下代码:
module.exports = {
chainWebpack(config) {
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap((options) => {
const compile = options.compiler.compile
options.compiler.compile = (template, info) => {
// 获取所有页面(排除所有组件),插入<NoticeBar />组件
if (
info.resourcePath?.match(/^pages/) &&
!info.resourcePath?.match(/\/components\//)
) {
template = template.trim()
template = template.replace(
/^<[\d\D]+?>/g,
(_) => `${_}<NoticeBar />
`
)
}
return compile(template, info)
}
return options
})
}
}
三、公告滚动条组件
<template>
<view class="wrapper" v-if="showNoticeBar">
<view class="notice-bar">
<view class="notice-title">
<image
class="notice-icon"
src="https://pubfile.bluemoon.com.cn/group1/M00/60/76/wKgwcGPyz-OAETXkAAAD3-2zYCI641.png"
/>
<text class="notice-title__txt">公告</text>
</view>
<view class="notice-content">
<view
class="notice-content__txt-wrapper"
:style="{
'animation-duration':
(animationDuration ? animationDuration : 10) + 's'
}"
>
<view
class="notice-content__txt"
v-for="(item, index) in list"
:key="index"
@click="toDetail(item)"
v-html="getTxt(item)"
>
</view>
</view>
</view>
<view class="notice-close" @click="closeNoticeBar">
<image
class="notice-icon"
src="https://pubfile.bluemoon.com.cn/group1/M00/60/78/wKgwb2Py0N-ACC2nAAABwyrvzsg318.png"
/>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {
ref,
computed,
onMounted,
getCurrentInstance,
watchEffect,
nextTick
} from '@vue/composition-api'
import { getMessageRemind } from '@/apis/message'
import { isMP } from '@/utils/index'
import { useNoticeBar } from '@/stores/noticeBar'
import { storeToRefs } from 'pinia'
const noticeBarStore = useNoticeBar()
const { showNoticeBar } = storeToRefs(noticeBarStore)
interface Notice {
content: string
currentGiveThumbFlag: boolean
giveThumbTotal: number
noticeState: number
noticeType: number
noticeTypeDesc: string
releaseChannel: string
releaseCycle: number
releaseTime: string
summary: string
sysNoticeId: string
title: string
triggerMode: number
updateTime: string
msgId: string
}
const instance = getCurrentInstance()?.proxy
const noticeBannerList = ref<Notice[]>([])
const list = computed(() => {
return noticeBannerList.value.concat(noticeBannerList.value)
})
// 数据变化时,重新计算滚动时间
watchEffect(async () => {
if (noticeBannerList.value.length > 0) {
await nextTick()
setAnimationDuration()
}
})
// 关闭滚动条
const closeNoticeBar = () => {
noticeBarStore.setShowNoticeBar(false)
}
// 获取公告数据
const queryMessage = async () => {
const clientType = isMP ? 2 : 3
const { data, isSuccess } = await getMessageRemind({ clientType })
if (!isSuccess) return
// 公告横幅
noticeBannerList.value =
data.unreadSysNoticeDetailList?.filter((item: Notice) => {
return item.releaseChannel.split(',').includes('4')
}) || []
if (noticeBannerList.value.length > 0) {
noticeBarStore.setShowNoticeBar(true)
} else {
noticeBarStore.setShowNoticeBar(false)
}
}
// 处理公告数据
const getTxt = (item: Notice) => {
const colon = item.title && item.summary ? ':' : ''
const txt = item.title + colon + item.summary
var reg = /<.*?>/g
const res = txt.replace(reg, '')
return res
}
// 根据文案宽度 动态设置滚动时间
const animationDuration = ref(10)
const setAnimationDuration = () => {
uni
.createSelectorQuery()
.in(instance)
.select('.notice-content__txt-wrapper')
.boundingClientRect((data: any) => {
const speed = 100
console.log('data', data, data.width / speed)
if (data.width) animationDuration.value = data.width / speed
})
.exec()
}
// 跳转详情页
const toDetail = (item: Notice) => {
uni.navigateTo({
url: `/pages/notice/index?sysNoticeId=${item.sysNoticeId}&msgId=${item.msgId}`
})
}
onMounted(async () => {
queryMessage()
})
// defineExpose({ open })
</script>
<style lang="scss" scoped>
.wrapper {
height: 80rpx;
}
.notice {
&-bar {
@include flex();
@include jc-between();
@include ai-center();
width: 100vw;
height: 80rpx;
padding: 22rpx 16rpx 22rpx 32rpx;
background: #fef0ee;
box-sizing: border-box;
font-size: 28rpx;
color: #d14a38;
position: fixed;
z-index: 9;
top: 0;
left: 0;
/* #ifdef H5 */
top: 88rpx;
/* #endif */
}
&-title {
@include flex();
@include ai-center();
&__txt {
margin-left: 8rpx;
font-weight: bold;
}
}
&-icon {
width: 36rpx;
height: 36rpx;
}
&-content {
@include flex-1();
margin: 0 16rpx;
overflow: hidden;
white-space: nowrap;
word-break: keep-all;
&__txt {
padding-right: 200rpx;
display: inline-block;
&-wrapper {
display: inline-block;
animation-name: u-loop-animation;
animation-duration: 10s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
}
}
}
@keyframes u-loop-animation {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-50%, 0, 0);
}
}
</style>