测试版
This commit is contained in:
249
src/components/NoticeBar.vue
Normal file
249
src/components/NoticeBar.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div v-if="activeNotices.length > 0"
|
||||
:class="['notice-bar', `notice-bar--${currentNotice.type || 'info'}`]">
|
||||
<!-- 图标 -->
|
||||
<span class="material-icons-round notice-bar__icon">campaign</span>
|
||||
|
||||
<!-- 滚动内容区域 -->
|
||||
<div class="notice-bar__content" ref="wrapRef">
|
||||
<div class="notice-bar__text" ref="textRef" :style="animationStyle">
|
||||
{{ currentNotice.content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 多条公告时显示计数 -->
|
||||
<span v-if="activeNotices.length > 1" class="notice-bar__count">
|
||||
{{ currentIndex + 1 }}/{{ activeNotices.length }}
|
||||
</span>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button v-if="closable" class="notice-bar__close" @click="handleClose"
|
||||
:title="t('notice.close')">
|
||||
<span class="material-icons-round text-base">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useNoticeStore } from '@/stores/noticeStore'
|
||||
|
||||
const props = defineProps({
|
||||
speed: { type: Number, default: 50 }, // 滚动速度 px/s
|
||||
closable: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const noticeStore = useNoticeStore()
|
||||
|
||||
const activeNotices = computed(() => noticeStore.activeNotices)
|
||||
|
||||
// 当前显示的公告索引(多条轮播)
|
||||
const currentIndex = ref(0)
|
||||
const currentNotice = computed(() => activeNotices.value[currentIndex.value] || {})
|
||||
|
||||
// 滚动动画
|
||||
const wrapRef = ref(null)
|
||||
const textRef = ref(null)
|
||||
const animationDuration = ref(10)
|
||||
const needScroll = ref(false)
|
||||
|
||||
const animationStyle = computed(() => {
|
||||
if (!needScroll.value) return {}
|
||||
return {
|
||||
animationDuration: `${animationDuration.value}s`,
|
||||
}
|
||||
})
|
||||
|
||||
// 计算文本是否需要滚动
|
||||
const calculateScroll = async () => {
|
||||
await nextTick()
|
||||
if (!wrapRef.value || !textRef.value) return
|
||||
|
||||
const wrapWidth = wrapRef.value.offsetWidth
|
||||
const textWidth = textRef.value.scrollWidth
|
||||
|
||||
if (textWidth > wrapWidth) {
|
||||
needScroll.value = true
|
||||
// 基于文本宽度和速度计算持续时间
|
||||
animationDuration.value = (textWidth + wrapWidth) / props.speed
|
||||
} else {
|
||||
needScroll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 多条公告自动轮播
|
||||
let rotateTimer = null
|
||||
|
||||
const startRotate = () => {
|
||||
stopRotate()
|
||||
if (activeNotices.value.length <= 1) return
|
||||
|
||||
rotateTimer = setInterval(() => {
|
||||
currentIndex.value = (currentIndex.value + 1) % activeNotices.value.length
|
||||
}, 8000) // 每 8 秒切换
|
||||
}
|
||||
|
||||
const stopRotate = () => {
|
||||
if (rotateTimer) {
|
||||
clearInterval(rotateTimer)
|
||||
rotateTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭当前公告
|
||||
const handleClose = () => {
|
||||
const notice = currentNotice.value
|
||||
if (notice && notice.id) {
|
||||
noticeStore.dismissNotice(notice.id)
|
||||
// 调整索引
|
||||
if (currentIndex.value >= activeNotices.value.length) {
|
||||
currentIndex.value = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听公告变化重新计算
|
||||
watch(currentNotice, () => {
|
||||
needScroll.value = false
|
||||
calculateScroll()
|
||||
}, { flush: 'post' })
|
||||
|
||||
watch(() => activeNotices.value.length, (len) => {
|
||||
if (len > 1) {
|
||||
startRotate()
|
||||
} else {
|
||||
stopRotate()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
calculateScroll()
|
||||
if (activeNotices.value.length > 1) {
|
||||
startRotate()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopRotate()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notice-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 44px;
|
||||
font-size: 15px;
|
||||
line-height: 44px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 类型样式 */
|
||||
.notice-bar--info {
|
||||
background-color: #eff6ff;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.notice-bar--info .notice-bar__icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.notice-bar--warning {
|
||||
background-color: #fffbeb;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.notice-bar--warning .notice-bar__icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.notice-bar--danger {
|
||||
background-color: rgb(253, 220, 220);
|
||||
color: #f15252;
|
||||
}
|
||||
|
||||
.notice-bar--danger .notice-bar__icon {
|
||||
color: #ff0000;
|
||||
}
|
||||
.notice-bar--urgent {
|
||||
background-color: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.notice-bar--urgent .notice-bar__icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* 图标 */
|
||||
.notice-bar__icon {
|
||||
font-size: 22px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.notice-bar__content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notice-bar__text {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 需要滚动时添加动画类 */
|
||||
.notice-bar__content .notice-bar__text {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.notice-bar__text[style*="animationDuration"] {
|
||||
animation-name: noticeScroll;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
padding-left: 100%;
|
||||
}
|
||||
|
||||
@keyframes noticeScroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 计数 */
|
||||
.notice-bar__count {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
margin-left: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.notice-bar__close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
color: inherit;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.notice-bar__close:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user