测试版

This commit is contained in:
2026-02-11 14:49:18 +08:00
parent bef5c2f437
commit 92780ef52e
6 changed files with 348 additions and 0 deletions

View 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>