| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- <script setup lang="ts">
- import EventCard from './components/EventCard.vue'
- import PasswordCard from './components/PasswordCard.vue'
- import FeatureUpdateCard from './components/FeatureUpdateCard.vue'
- import { useNotificationMessage } from '@/stores/modules/notificationMessage'
- import { cloneDeep } from 'lodash'
- // import { DynamicScroller, DynamicScrollerItem } from 'vue3-virtual-scroller'
- import 'vue3-virtual-scroller/dist/vue3-virtual-scroller.css'
- import { formatTimezone } from '@/utils/tools'
- const notificationMsgStore = useNotificationMessage()
- const props = withDefaults(
- defineProps<{
- data: any
- isObserver?: boolean // 是否开启监听
- isScrollPadding?: boolean // 是否有padding
- updateReadCardsOnChange?: boolean // 是否在数据变化时请求接口更新已读卡片
- topOffset?: number // 应减去高度
- isShowInsertionTime?: boolean // 是否显示插入时间
- }>(),
- {
- isObserver: true,
- isScrollPadding: false,
- updateReadCardsOnChange: true,
- topOffset: 220
- }
- )
- const pageData = ref<any[]>([])
- const emit = defineEmits<{
- seeAll: []
- hasCardRead: []
- viewMore: []
- jumpTracking: []
- loading: []
- }>()
- const handleSeeAll = () => {
- emit('seeAll')
- }
- // 创建一个新的 IntersectionObserver 实例
- const observer = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- const cardId = entry.target?.dataset?.cardId
- if (entry.isIntersecting) {
- // 将卡片设置为已经展示
- notificationMsgStore.setReadCardMap(cardId)
- const index = pageData.value.findIndex((item) => item.info.id === cardId)
- if (index > -1) {
- // 在1秒钟后将消息卡片设置为已读
- setTimeout(() => {
- pageData.value[index].info.isRead = true
- emit('hasCardRead')
- }, 400)
- }
- }
- })
- },
- {
- // 配置选项
- root: null, // 使用视窗作为根元素
- threshold: 0.1 // 当至少10%的元素进入视图时触发回调
- }
- )
- const curPageAllCards = ref<any>([])
- // 监听元素是否在可视区域内
- const watchCards = () => {
- curPageAllCards.value = document?.querySelectorAll('.notification-message-card')
- curPageAllCards.value.forEach((card: any) => {
- // const index = notificationMsgStore.notificationMsgList.indexOf(card.dataset.cardId)
- // index < 0 &&
- if (card.dataset.cardIsread === 'false' && card.dataset.cardId) {
- notificationMsgStore.concatNotificationMsgList([card.dataset.cardId])
- observer.observe(card)
- }
- })
- }
- const clearReadData = (data) => {
- const curData = data || []
- // 将当前页面剩余未读消息id从监听列表中移除
- const idsToRemove = curData.map((item: any) => item.info.id)
- notificationMsgStore.removeNotificationMsgList(idsToRemove)
- // 清除当前页面的所有卡片的监听
- curPageAllCards.value.forEach((card: any) => {
- observer.unobserve(card)
- })
- }
- const compareIdsInArrays = (arr1, arr2) => {
- // 如果两个数组长度不同,则它们不可能有相同的id集合
- if (arr1.length !== arr2.length) {
- return false
- }
- // 提取两个数组中的所有有效id并转换为Set
- const ids1 = new Set(arr1.filter((item) => item.info && item.info.id).map((item) => item.info.id))
- const ids2 = new Set(arr2.filter((item) => item.info && item.info.id).map((item) => item.info.id))
- // 比较两个Set的大小是否相同
- if (ids1.size !== ids2.size) {
- return false
- }
- // 检查ids1中的每个id是否都存在于ids2中
- for (let id of ids1) {
- if (!ids2.has(id)) {
- return false
- }
- }
- return true
- }
- const handleData = (data) => {
- data.map((item) => {
- item.id = String(Math.random()).slice(-10)
- })
- return data
- }
- const initData = () => {
- const watchOptions = { deep: true, immediate: true }
- const handleDataChange = (newData, oldData) => {
- // 如果id相等,则不需要更新页面数据
- if (compareIdsInArrays(oldData || [], newData || [])) return
- pageData.value = handleData(cloneDeep(newData))
- // 清除旧数据中的卡片监听
- clearReadData(oldData)
- // 请求接口将旧数据中的新已读卡片上传服务器
- if (props.data.updateReadCardsOnChange) {
- notificationMsgStore.markMessageAsRead()
- }
- // 重新监听新数据中的卡片
- nextTick(() => {
- setTimeout(() => {
- watchCards()
- }, 500)
- })
- }
- // 需要监听卡片已读未读状态
- if (props.isObserver) {
- watch(
- () => props.data,
- (newVal, oldVal) => {
- handleDataChange(newVal, oldVal)
- },
- watchOptions
- )
- } else {
- // 不需要监听
- watch(
- () => props.data,
- (newVal) => {
- pageData.value = handleData(cloneDeep(newVal))
- },
- watchOptions
- )
- }
- }
- initData()
- // 将数据中的消息置为已读
- const setAllMessageRead = () => {
- pageData.value.forEach((item) => {
- item.info.isRead = true
- })
- }
- // 定时将消息卡片未读的置为已读,五分钟一次
- let timer = null
- onMounted(() => {
- if (props.isObserver) {
- timer = setInterval(() => {
- notificationMsgStore.markMessageAsRead()
- }, 300000)
- }
- })
- onUnmounted(() => {
- if (props.isObserver) {
- notificationMsgStore.markMessageAsRead()
- clearReadData(pageData.value)
- clearInterval(timer)
- }
- })
- const handleViewMore = () => {
- emit('viewMore')
- }
- const parentHeight = computed(() => {
- return (window.innerHeight || document.documentElement.clientHeight) - props.topOffset
- })
- const scrollParentBoxStyle = computed(() => {
- return props.topOffset ? { height: `${parentHeight.value}px` } : {}
- })
- const scrollContainerRef = ref<HTMLElement | null>(null)
- const loading = ref(false)
- const finished = ref(false)
- const prevScrollTop = ref(0)
- const loadMore = async () => {
- if (loading.value || finished.value) return
- loading.value = true
- emit('loading')
- }
- const onScroll = () => {
- const el = scrollContainerRef.value
- if (!el || loading.value || finished.value) return
- prevScrollTop.value = el.scrollTop + 50
- const threshold = 50 // 提前50px触发
- if (el.scrollHeight - el.scrollTop - el.clientHeight <= threshold) {
- loadMore()
- }
- }
- const sentinel = ref<HTMLElement | null>(null)
- const adjustScrollTop = () => {
- const el = scrollContainerRef.value
- if (el) {
- // 如果滚动容器存在,调整其 scrollTop
- el.scrollTop = prevScrollTop.value
- }
- }
- defineExpose({
- loading,
- finished,
- scrollContainerRef,
- adjustScrollTop,
- setAllMessageRead
- })
- </script>
- <template>
- <div class="scroller" :style="scrollParentBoxStyle" ref="scrollContainerRef" @scroll="onScroll">
- <template v-for="item in pageData" :key="item.id">
- <div
- :class="{ 'scroll-padding': props.isScrollPadding }"
- :item="item"
- :size-dependencies="[item.info.isRead]"
- >
- <div
- class="notification-message-card"
- :data-card-id="item.info.id"
- :data-card-isread="item.info.isRead"
- >
- <EventCard
- @seeAll="handleSeeAll"
- @jump-tracking="emit('jumpTracking')"
- v-if="item.notificationType === 'event'"
- :data="item.info"
- />
- <PasswordCard v-else-if="item.notificationType === 'password'" :data="item.info" />
- <FeatureUpdateCard
- @view-more="handleViewMore"
- v-else-if="item.notificationType === 'feature'"
- :data="item.info"
- />
- <slot></slot>
- <div class="insertion-time" v-if="isShowInsertionTime">
- {{ formatTimezone(item.info.first_notifiation_date) }}
- </div>
- </div>
- </div>
- </template>
- <div
- class="footer"
- v-if="pageData?.[0]?.notificationType !== 'feature' && pageData?.length > 0"
- >
- <el-divider v-if="loading"> loading... </el-divider>
- <el-divider v-if="finished && pageData.length > 0">
- Only display the message data within three months
- </el-divider>
- </div>
- </div>
- </template>
- <style lang="scss" scoped>
- .scroller {
- width: 100%;
- overflow-y: auto;
- .scroll-padding {
- padding: 0px 140px 0px 16px;
- }
- .footer {
- padding: 0 16px;
- text-align: center;
- }
- }
- .notification-message-card {
- position: relative;
- .insertion-time {
- position: absolute;
- right: 0;
- top: 4px;
- font-size: 12px;
- color: var(--color-neutral-2);
- }
- }
- </style>
|