feat: 顶部通知铃铛与站内通知中心
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m7s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m7s
This commit is contained in:
@@ -1,276 +1,168 @@
|
||||
<template>
|
||||
<div
|
||||
class="notice"
|
||||
v-show="visible"
|
||||
:style="{
|
||||
transform: show ? 'scaleY(1)' : 'scaleY(0.9)',
|
||||
opacity: show ? 1 : 0
|
||||
}"
|
||||
@click.stop=""
|
||||
>
|
||||
<div class="header">
|
||||
<span class="text">{{ $t('notice.title') }}</span>
|
||||
<span class="btn">{{ $t('notice.btnRead') }}</span>
|
||||
</div>
|
||||
<ul class="bar">
|
||||
<li
|
||||
v-for="(item, index) in barList"
|
||||
:key="index"
|
||||
:class="{ active: barActiveIndex === index }"
|
||||
@click="changeBar(index)"
|
||||
>
|
||||
{{ item.name }} ({{ item.num }})
|
||||
</li>
|
||||
</ul>
|
||||
<div class="content">
|
||||
<div class="scroll">
|
||||
<!-- 通知 -->
|
||||
<ul class="notice-list" v-show="barActiveIndex === 0">
|
||||
<li v-for="(item, index) in noticeList" :key="index">
|
||||
<div
|
||||
class="icon"
|
||||
:style="{ background: getNoticeStyle(item.type).backgroundColor + '!important' }"
|
||||
>
|
||||
<i
|
||||
class="iconfont-sys"
|
||||
:style="{ color: getNoticeStyle(item.type).iconColor + '!important' }"
|
||||
v-html="getNoticeStyle(item.type).icon"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 消息 -->
|
||||
<ul class="user-list" v-show="barActiveIndex === 1">
|
||||
<li v-for="(item, index) in msgList" :key="index">
|
||||
<div class="avatar">
|
||||
<span>{{ item.title.slice(0, 1) }}</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 待办 -->
|
||||
<ul class="base" v-show="barActiveIndex === 3">
|
||||
<li v-for="(item, index) in pendingList" :key="index">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="visible" class="notice-overlay" @click="close">
|
||||
<div class="notice" @click.stop>
|
||||
<div class="header">
|
||||
<span class="text">通知</span>
|
||||
<button
|
||||
v-if="hasAuth(NOTIFICATION_PERMISSIONS.readAll)"
|
||||
class="read-all"
|
||||
type="button"
|
||||
@click="handleMarkAllRead"
|
||||
>
|
||||
全部已读
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="empty-tips" v-show="barActiveIndex === 0 && noticeList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
<div class="bar">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
type="button"
|
||||
:class="{ active: activeCategory === tab.value }"
|
||||
@click="activeCategory = tab.value"
|
||||
>
|
||||
{{ tab.label }}<span v-if="tab.count"> ({{ tab.count }})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div v-loading="notificationStore.loading" class="scroll">
|
||||
<button
|
||||
v-for="item in filteredItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
:class="['notification-item', { unread: !isRead(item) }]"
|
||||
@click="handleNotificationClick(item)"
|
||||
>
|
||||
<span :class="['icon', `severity-${item.severity || 'info'}`]">
|
||||
<i class="iconfont-sys"></i>
|
||||
</span>
|
||||
<span class="text">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>{{ item.content }}</small>
|
||||
<time>{{ item.created_at || '-' }}</time>
|
||||
</span>
|
||||
<span v-if="!isRead(item)" class="unread-dot" />
|
||||
</button>
|
||||
|
||||
<div v-if="!filteredItems.length" class="empty-tips">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>暂无通知</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tips" v-show="barActiveIndex === 1 && msgList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
</div>
|
||||
<div class="empty-tips" v-show="barActiveIndex === 2 && pendingList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
<div class="btn-wrapper">
|
||||
<ElButton class="view-all" @click="handleViewAll">进入通知中心</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-wrapper">
|
||||
<el-button class="view-all" @click="handleViewAll" v-ripple>
|
||||
{{ $t('notice.viewAll') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 100px"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { NotificationService } from '@/api/modules'
|
||||
import type { NotificationCategory, NotificationItem } from '@/types/api'
|
||||
import { useNotificationStore } from '@/store/modules/notification'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification'
|
||||
import { navigateNotificationTarget } from '@/utils/business/notificationNavigation'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
const router = useRouter()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { hasAuth } = useAuth()
|
||||
const activeCategory = ref<'all' | NotificationCategory>('all')
|
||||
const visible = computed(() => props.modelValue)
|
||||
|
||||
const categoryMatches = (item: NotificationItem, category: string) => {
|
||||
if (category === 'all') return true
|
||||
if (category === 'system') return item.category === 'sync' || item.category === 'system'
|
||||
if (category === 'expiry') return item.category === 'expiry' || item.category === 'expiring'
|
||||
return item.category === category
|
||||
}
|
||||
|
||||
const isRead = (item: NotificationItem) =>
|
||||
item.read === true || item.is_read === true || item.read_status === 'read'
|
||||
|
||||
const filteredItems = computed(() =>
|
||||
notificationStore.summary.items.filter((item) => categoryMatches(item, activeCategory.value))
|
||||
)
|
||||
|
||||
const tabs = computed(() => {
|
||||
const categories = notificationStore.summary.categories || {}
|
||||
return [
|
||||
{ value: 'all', label: '全部', count: notificationStore.unreadCount },
|
||||
{ value: 'approval', label: '审批', count: categories.approval || 0 },
|
||||
{ value: 'expiry', label: '临期', count: categories.expiry || categories.expiring || 0 },
|
||||
{
|
||||
value: 'system',
|
||||
label: '同步/系统',
|
||||
count: (categories.sync || 0) + (categories.system || 0)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const close = () => emit('update:modelValue', false)
|
||||
|
||||
const handleViewAll = () => {
|
||||
close()
|
||||
void router.push('/notifications')
|
||||
}
|
||||
|
||||
const handleMarkAllRead = async () => {
|
||||
try {
|
||||
const response = await notificationStore.markAllRead()
|
||||
if (response.code !== 0) ElMessage.error(response.msg || '全部已读失败')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '全部已读失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotificationClick = async (item: NotificationItem) => {
|
||||
if (!hasAuth(NOTIFICATION_PERMISSIONS.read)) {
|
||||
ElMessage.warning('您没有标记通知已读的权限')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const readResponse = await notificationStore.markRead(item.id)
|
||||
if (readResponse.code !== 0) {
|
||||
ElMessage.error(readResponse.msg || '标记已读失败')
|
||||
return
|
||||
}
|
||||
const targetResponse = await NotificationService.getTarget(item.id)
|
||||
if (targetResponse.code !== 0 || !targetResponse.data) {
|
||||
ElMessage.info(item.content || '该通知暂无可跳转目标')
|
||||
return
|
||||
}
|
||||
if (!navigateNotificationTarget(router, targetResponse.data)) {
|
||||
ElMessage.info(targetResponse.data.message || item.content || '该通知暂无可跳转目标')
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '处理通知失败')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
showNotice(props.value)
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) {
|
||||
activeCategory.value = 'all'
|
||||
void notificationStore.refreshSummary().catch((error) => {
|
||||
ElMessage.error(error?.message || '获取通知失败')
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const show = ref(false)
|
||||
const visible = ref(false)
|
||||
const barActiveIndex = ref(0)
|
||||
const pendingList: any = []
|
||||
|
||||
const barList = ref([
|
||||
{
|
||||
name: computed(() => t('notice.bar[0]')),
|
||||
num: 1
|
||||
},
|
||||
{
|
||||
name: computed(() => t('notice.bar[1]')),
|
||||
num: 1
|
||||
},
|
||||
{
|
||||
name: computed(() => t('notice.bar[2]')),
|
||||
num: 0
|
||||
}
|
||||
])
|
||||
|
||||
const noticeList = [
|
||||
{
|
||||
title: '新增国际化',
|
||||
time: '2024-6-13 0:10',
|
||||
type: 'notice'
|
||||
},
|
||||
{
|
||||
title: '冷月呆呆给你发了一条消息',
|
||||
time: '2024-4-21 8:05',
|
||||
type: 'message'
|
||||
},
|
||||
{
|
||||
title: '小肥猪关注了你',
|
||||
time: '2020-3-17 21:12',
|
||||
type: 'collection'
|
||||
},
|
||||
{
|
||||
title: '新增使用文档',
|
||||
time: '2024-02-14 0:20',
|
||||
type: 'notice'
|
||||
},
|
||||
{
|
||||
title: '小肥猪给你发了一封邮件',
|
||||
time: '2024-1-20 0:15',
|
||||
type: 'email'
|
||||
},
|
||||
{
|
||||
title: '菜单mock本地真实数据',
|
||||
time: '2024-1-17 22:06',
|
||||
type: 'notice'
|
||||
}
|
||||
]
|
||||
const msgList: any = [
|
||||
{
|
||||
title: '池不胖 关注了你',
|
||||
time: '2021-2-26 23:50'
|
||||
},
|
||||
{
|
||||
title: '唐不苦 关注了你',
|
||||
time: '2021-2-21 8:05'
|
||||
},
|
||||
{
|
||||
title: '中小鱼 关注了你',
|
||||
time: '2020-1-17 21:12'
|
||||
},
|
||||
{
|
||||
title: '何小荷 关注了你',
|
||||
time: '2021-01-14 0:20'
|
||||
},
|
||||
{
|
||||
title: '誶誶淰 关注了你',
|
||||
time: '2020-12-20 0:15'
|
||||
},
|
||||
{
|
||||
title: '冷月呆呆 关注了你',
|
||||
time: '2020-12-17 22:06'
|
||||
}
|
||||
]
|
||||
|
||||
const changeBar = (index: number) => {
|
||||
barActiveIndex.value = index
|
||||
}
|
||||
|
||||
const getRandomColor = () => {
|
||||
const index = Math.floor(Math.random() * AppConfig.systemMainColor.length)
|
||||
return AppConfig.systemMainColor[index]
|
||||
}
|
||||
|
||||
const noticeStyleMap = {
|
||||
email: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-warning))',
|
||||
backgroundColor: 'rgb(var(--art-bg-warning))'
|
||||
},
|
||||
message: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-success))',
|
||||
backgroundColor: 'rgb(var(--art-bg-success))'
|
||||
},
|
||||
collection: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-danger))',
|
||||
backgroundColor: 'rgb(var(--art-bg-danger))'
|
||||
},
|
||||
user: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-info))',
|
||||
backgroundColor: 'rgb(var(--art-bg-info))'
|
||||
},
|
||||
notice: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-primary))',
|
||||
backgroundColor: 'rgb(var(--art-bg-primary))'
|
||||
}
|
||||
}
|
||||
|
||||
const getNoticeStyle = (type: string) => {
|
||||
const defaultStyle = {
|
||||
icon: '',
|
||||
iconColor: '#FFFFFF',
|
||||
backgroundColor: getRandomColor()
|
||||
}
|
||||
|
||||
const style = noticeStyleMap[type as keyof typeof noticeStyleMap] || defaultStyle
|
||||
|
||||
return {
|
||||
...style,
|
||||
backgroundColor: style.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
const showNotice = (open: boolean) => {
|
||||
if (open) {
|
||||
visible.value = open
|
||||
setTimeout(() => {
|
||||
show.value = open
|
||||
}, 5)
|
||||
} else {
|
||||
show.value = open
|
||||
setTimeout(() => {
|
||||
visible.value = open
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看全部
|
||||
const handleViewAll = () => {
|
||||
switch (barActiveIndex.value) {
|
||||
case 0:
|
||||
handleNoticeAll()
|
||||
break
|
||||
case 1:
|
||||
handleMsgAll()
|
||||
break
|
||||
case 2:
|
||||
handlePendingAll()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleNoticeAll = () => {}
|
||||
const handleMsgAll = () => {}
|
||||
const handlePendingAll = () => {}
|
||||
onMounted(() => {
|
||||
void notificationStore.refreshUnreadCount().catch(() => undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user