Files
his/openhis-ui-vue3/src/layout/components/HeaderNotice/index.vue
chenqi 58669ce9b6 feat(notice): 重构通知公告功能实现消息中心
- 新增顶部公告/通知列表获取接口 listNoticeTop
- 新增标记单条公告为已读接口 markNoticeRead
- 新增批量标记公告为已读接口 markNoticeReadAll
- 重构 HeaderNotice 组件实现完整的消息中心功能
- 添加标签页分类显示全部、通知、公告三种类型
- 实现消息实时未读数统计和标记已读功能
- 优化消息展示界面增加图标区分通知和公告类型
- 更新 Navbar 组件集成新的消息中心功能
- 调整布局样式适配消息中心组件
- 修复设置面板导航类型配置问题
- 添加 Chrome 风格标签页样式支持
2026-06-04 12:57:04 +08:00

361 lines
8.3 KiB
Vue

<template>
<div>
<el-popover
ref="noticePopover"
placement="bottom-end"
:width="360"
trigger="click"
v-model:visible="noticeVisible"
popper-class="notice-popover"
:show-arrow="false"
>
<template #default>
<div class="notice-popover-content">
<!-- 头部 -->
<div class="notice-header">
<span class="notice-header-title">消息中心</span>
<el-badge :value="noticeStore.unreadCount" :hidden="noticeStore.unreadCount === 0" :max="99">
<span />
</el-badge>
<span class="notice-header-action" @click="handleReadAll">全部已读</span>
</div>
<!-- Tabs -->
<el-tabs v-model="activeTab" class="notice-tabs">
<el-tab-pane name="all">
<template #label>
全部
<el-badge v-if="allUnread > 0" :value="allUnread" :max="99" class="tab-badge" />
</template>
</el-tab-pane>
<el-tab-pane name="notice">
<template #label>
通知
<el-badge v-if="noticeUnread > 0" :value="noticeUnread" :max="99" class="tab-badge" />
</template>
</el-tab-pane>
<el-tab-pane name="announce">
<template #label>
公告
<el-badge v-if="announceUnread > 0" :value="announceUnread" :max="99" class="tab-badge" />
</template>
</el-tab-pane>
</el-tabs>
<!-- 列表 -->
<el-scrollbar max-height="350px" class="notice-scroll">
<div v-if="filteredList.length === 0" class="notice-empty">
<el-empty description="暂无消息" :image-size="60" />
</div>
<div
v-for="item in filteredList"
:key="item.noticeId"
class="notice-item"
:class="{ 'is-read': isRead(item) }"
@click="handleRead(item)"
>
<div class="notice-item-icon">
<el-icon v-if="item.noticeType === '1'" class="icon-notice"><Bell /></el-icon>
<el-icon v-else class="icon-announce"><Notification /></el-icon>
</div>
<div class="notice-item-body">
<div class="notice-item-title">
<span>{{ item.noticeTitle }}</span>
<el-tag v-if="!isRead(item)" type="danger" size="small" effect="dark" class="unread-tag">未读</el-tag>
</div>
<div class="notice-item-time">{{ formatTime(item.createTime) }}</div>
</div>
</div>
</el-scrollbar>
<!-- 底部 -->
<div class="notice-footer" @click="openNoticeCenter">
<span>查看全部</span>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</template>
<template #reference>
<div class="left-action-item">
<el-badge :value="noticeStore.unreadCount" :hidden="noticeStore.unreadCount === 0" :max="99">
<el-icon :size="18"><Bell /></el-icon>
</el-badge>
</div>
</template>
</el-popover>
<!-- 详情抽屉 -->
<DetailView ref="detailViewRef" />
</div>
</template>
<script setup>
import DetailView from './DetailView.vue'
import useNoticeStore from '@/store/modules/notice'
import { Bell, Notification, ArrowRight } from '@element-plus/icons-vue'
const noticeStore = useNoticeStore()
const noticeVisible = ref(false)
const activeTab = ref('all')
const detailViewRef = ref(null)
// 过滤后的列表
const filteredList = computed(() => {
const list = noticeStore.noticeList
if (activeTab.value === 'notice') {
return list.filter(n => n.noticeType === '1')
}
if (activeTab.value === 'announce') {
return list.filter(n => n.noticeType === '2')
}
return list
})
// 各类型未读数
const allUnread = computed(() => noticeStore.unreadCount)
const noticeUnread = computed(() => {
return noticeStore.noticeList.filter(n => n.noticeType === '1' && !isRead(n)).length
})
const announceUnread = computed(() => {
return noticeStore.noticeList.filter(n => n.noticeType === '2' && !isRead(n)).length
})
function isRead(item) {
return item.isRead || item.readFlag === '1' || noticeStore.readIds.has(item.noticeId)
}
// 格式化时间
function formatTime(time) {
if (!time) return ''
const d = new Date(time)
const now = new Date()
const diff = now - d
if (diff < 60000) return '刚刚'
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前'
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
// 点击单条 → 标记已读 + 打开详情
function handleRead(item) {
if (!isRead(item)) {
noticeStore.markRead(item.noticeId)
}
detailViewRef.value?.open(item)
}
// 全部已读
function handleReadAll() {
noticeStore.markAllRead()
}
// 查看全部 → 关闭弹窗,跳转公告页面
function openNoticeCenter() {
noticeVisible.value = false
// 可跳转到公告管理页面或打开全屏面板
}
// 定时刷新未读数
let timer = null
onMounted(() => {
noticeStore.fetchNotices()
timer = setInterval(() => {
noticeStore.fetchUnreadCount()
}, 60000) // 每分钟刷新一次
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
<style lang="scss" scoped>
.left-action-item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 10px;
height: 50px;
font-size: 16px;
color: #606266;
cursor: pointer;
transition: background 0.3s;
&:hover {
background-color: #f6f6f6;
}
}
:deep(.notice-popover) {
padding: 0 !important;
}
.notice-popover-content {
display: flex;
flex-direction: column;
}
.notice-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px 10px;
border-bottom: 1px solid #f0f0f0;
.notice-header-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
}
.notice-header-action {
font-size: 12px;
color: #3B82F6;
cursor: pointer;
&:hover {
color: #2563EB;
}
}
}
.notice-tabs {
:deep(.el-tabs__header) {
margin: 0;
padding: 0 16px;
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
:deep(.el-tabs__item) {
padding: 0 12px;
height: 40px;
line-height: 40px;
font-size: 13px;
}
.tab-badge {
margin-left: 4px;
:deep(.el-badge__content) {
font-size: 10px;
}
}
}
.notice-scroll {
max-height: 350px;
}
.notice-empty {
padding: 30px 0;
}
.notice-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid #f5f5f5;
&:hover {
background: #f7f8fa;
}
&:last-child {
border-bottom: none;
}
&.is-read {
opacity: 0.55;
.notice-item-title span {
color: #999;
}
}
.notice-item-icon {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
.icon-notice {
background: #E0F2FE;
color: #0284C7;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 16px;
}
.icon-announce {
background: #ECFDF5;
color: #059669;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 16px;
}
}
.notice-item-body {
flex: 1;
min-width: 0;
}
.notice-item-title {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
span {
flex: 1;
font-size: 13px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.unread-tag {
flex-shrink: 0;
}
}
.notice-item-time {
font-size: 11px;
color: #aaa;
}
}
.notice-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px;
border-top: 1px solid #f0f0f0;
font-size: 13px;
color: #3B82F6;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #f7f8fa;
}
}
</style>