feat(notice): 重构通知公告功能实现消息中心
- 新增顶部公告/通知列表获取接口 listNoticeTop - 新增标记单条公告为已读接口 markNoticeRead - 新增批量标记公告为已读接口 markNoticeReadAll - 重构 HeaderNotice 组件实现完整的消息中心功能 - 添加标签页分类显示全部、通知、公告三种类型 - 实现消息实时未读数统计和标记已读功能 - 优化消息展示界面增加图标区分通知和公告类型 - 更新 Navbar 组件集成新的消息中心功能 - 调整布局样式适配消息中心组件 - 修复设置面板导航类型配置问题 - 添加 Chrome 风格标签页样式支持
This commit is contained in:
@@ -97,6 +97,7 @@ export function sign(practitionerId, mac, ip) {
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
// 锁屏解锁(验证登录状态)
|
||||
export function unlockScreen(password) {
|
||||
return request({
|
||||
|
||||
@@ -108,3 +108,29 @@ export function getReadNoticeIds() {
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取顶部公告/通知列表(最新N条)
|
||||
export function listNoticeTop(query) {
|
||||
return request({
|
||||
url: '/system/notice/public/top',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 标记单条公告/通知为已读
|
||||
export function markNoticeRead(noticeId) {
|
||||
return request({
|
||||
url: '/system/notice/public/read/' + noticeId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 批量标记公告/通知为已读(逗号分隔的ID字符串)
|
||||
export function markNoticeReadAll(noticeIds) {
|
||||
return request({
|
||||
url: '/system/notice/public/read/all',
|
||||
method: 'post',
|
||||
data: noticeIds
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,184 +1,360 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover ref="noticePopover" placement="bottom-end" :width="320" trigger="manual" v-model:visible="noticeVisible" popper-class="notice-popover">
|
||||
<!-- 弹出内容 -->
|
||||
<div class="notice-header">
|
||||
<span class="notice-title">通知公告</span>
|
||||
<span class="notice-mark-all" @click="markAllRead">全部已读</span>
|
||||
</div>
|
||||
<div v-if="noticeLoading" class="notice-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon> 加载中...
|
||||
</div>
|
||||
<div v-else-if="noticeList.length === 0" class="notice-empty">
|
||||
<el-icon style="font-size:24px;display:block;margin-bottom:6px;"><Postcard /></el-icon>
|
||||
暂无公告
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': item.isRead }" @click="previewNotice(item)">
|
||||
<el-tag size="small" :type="item.noticeType === '1' ? 'warning' : 'success'" class="notice-tag">
|
||||
{{ item.noticeType === '1' ? '通知' : '公告' }}
|
||||
</el-tag>
|
||||
<span class="notice-item-title">{{ item.noticeTitle }}</span>
|
||||
<span class="notice-item-date">{{ item.createTime }}</span>
|
||||
</div>
|
||||
</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="right-menu-item hover-effect notice-trigger" @mouseenter="onNoticeEnter" @mouseleave="onNoticeLeave">
|
||||
<svg-icon icon-class="bell" />
|
||||
<span v-if="unreadCount > 0" class="notice-badge">{{ unreadCount }}</span>
|
||||
<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>
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
<notice-detail-view ref="noticeViewRef" />
|
||||
<!-- 详情抽屉 -->
|
||||
<DetailView ref="detailViewRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NoticeDetailView from './DetailView'
|
||||
import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
|
||||
import DetailView from './DetailView.vue'
|
||||
import useNoticeStore from '@/store/modules/notice'
|
||||
import { Bell, Notification, ArrowRight } from '@element-plus/icons-vue'
|
||||
|
||||
const noticePopover = ref(null)
|
||||
const noticeList = ref([])
|
||||
const unreadCount = ref(0)
|
||||
const noticeLoading = ref(false)
|
||||
const noticeStore = useNoticeStore()
|
||||
const noticeVisible = ref(false)
|
||||
const noticeLeaveTimer = ref(null)
|
||||
const { proxy } = getCurrentInstance()
|
||||
const activeTab = ref('all')
|
||||
const detailViewRef = ref(null)
|
||||
|
||||
// 加载顶部公告列表
|
||||
function loadNoticeTop() {
|
||||
noticeLoading.value = true
|
||||
listNoticeTop().then(res => {
|
||||
noticeList.value = res.data || []
|
||||
unreadCount.value = res.unreadCount !== undefined ? res.unreadCount : noticeList.value.filter(n => !n.isRead).length
|
||||
}).finally(() => {
|
||||
noticeLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => loadNoticeTop())
|
||||
|
||||
// 鼠标移入铃铛区域
|
||||
function onNoticeEnter() {
|
||||
clearTimeout(noticeLeaveTimer.value)
|
||||
noticeVisible.value = true
|
||||
nextTick(() => {
|
||||
const popper = noticePopover.value?.popperRef?.contentRef
|
||||
if (popper && !popper._noticeBound) {
|
||||
popper._noticeBound = true
|
||||
popper.addEventListener('mouseenter', () => clearTimeout(noticeLeaveTimer.value))
|
||||
popper.addEventListener('mouseleave', () => {
|
||||
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 100)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 鼠标离开铃铛区域
|
||||
function onNoticeLeave() {
|
||||
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 150)
|
||||
}
|
||||
|
||||
// 预览公告详情
|
||||
function previewNotice(item) {
|
||||
if (!item.isRead) {
|
||||
markNoticeRead(item.noticeId).catch(() => {})
|
||||
const idx = noticeList.value.indexOf(item)
|
||||
if (idx !== -1) noticeList.value[idx] = { ...item, isRead: true }
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1)
|
||||
// 过滤后的列表
|
||||
const filteredList = computed(() => {
|
||||
const list = noticeStore.noticeList
|
||||
if (activeTab.value === 'notice') {
|
||||
return list.filter(n => n.noticeType === '1')
|
||||
}
|
||||
proxy.$refs["noticeViewRef"].open(item.noticeId)
|
||||
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 markAllRead() {
|
||||
const ids = noticeList.value.map(n => n.noticeId).join(',')
|
||||
if (!ids) return
|
||||
markNoticeReadAll(ids).catch(() => {})
|
||||
noticeList.value = noticeList.value.map(n => ({ ...n, isRead: true }))
|
||||
unreadCount.value = 0
|
||||
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>
|
||||
.notice-trigger {
|
||||
position: relative;
|
||||
transform: translateX(-6px);
|
||||
.svg-icon { width: 1.2em; height: 1.2em; vertical-align: -0.2em; }
|
||||
.notice-badge {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: -3px;
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
padding: 0 4px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.notice-popover { padding: 0 !important; }
|
||||
.notice-popover .notice-header {
|
||||
|
||||
: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: 10px 14px;
|
||||
background: #f7f9fb;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
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-popover .notice-mark-all {
|
||||
font-size: 12px;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: normal;
|
||||
|
||||
.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-popover .notice-mark-all:hover { color: #2b7cc1; }
|
||||
.notice-popover .notice-loading,
|
||||
.notice-popover .notice-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.notice-popover .notice-item {
|
||||
|
||||
.notice-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 13px;
|
||||
color: #3B82F6;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.notice-popover .notice-item:last-child { border-bottom: none; }
|
||||
.notice-popover .notice-item:hover { background: #f7f9fb; }
|
||||
.notice-popover .notice-item.is-read .notice-tag,
|
||||
.notice-popover .notice-item.is-read .notice-item-title,
|
||||
.notice-popover .notice-item.is-read .notice-item-date { opacity: 0.45; filter: grayscale(1); color: #999; }
|
||||
.notice-popover .notice-tag { flex-shrink: 0; }
|
||||
.notice-popover .notice-item-title {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.notice-popover .notice-item-date {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- æœç´¢å’Œå…¬å‘Šé€šçŸ¥ -->
|
||||
<!-- 搜索和公告通知 -->
|
||||
<div class="left-actions">
|
||||
<template v-if="appStore.device !== 'mobile'">
|
||||
<header-search
|
||||
@@ -19,27 +19,67 @@
|
||||
class="left-action-item"
|
||||
/>
|
||||
</template>
|
||||
<!-- 公告和通知按钮 -->
|
||||
<el-tooltip
|
||||
content="公告/通知"
|
||||
placement="bottom"
|
||||
<!-- 消息通知 -->
|
||||
<el-popover
|
||||
placement="bottom-end"
|
||||
:width="360"
|
||||
trigger="click"
|
||||
v-model:visible="noticeVisible"
|
||||
popper-class="notice-popover"
|
||||
:show-arrow="false"
|
||||
>
|
||||
<div
|
||||
class="left-action-item notice-btn"
|
||||
@click="openNoticePanel"
|
||||
>
|
||||
<el-badge
|
||||
:value="unreadCount"
|
||||
:hidden="unreadCount === 0"
|
||||
class="notice-badge"
|
||||
>
|
||||
<el-icon><Bell /></el-icon>
|
||||
</el-badge>
|
||||
<div class="notice-popover-content">
|
||||
<div class="notice-header">
|
||||
<span class="notice-header-title">消息中心</span>
|
||||
<span class="notice-header-action" @click="handleNoticeReadAll">全部已读</span>
|
||||
</div>
|
||||
<el-tabs v-model="noticeTab" class="notice-tabs">
|
||||
<el-tab-pane name="all">
|
||||
<template #label>全部<el-badge v-if="noticeStore.unreadCount > 0" :value="noticeStore.unreadCount" :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">
|
||||
<div v-if="noticeFilteredList.length === 0" class="notice-empty">
|
||||
<el-empty description="暂无消息" :image-size="60" />
|
||||
</div>
|
||||
<div
|
||||
v-for="item in noticeFilteredList"
|
||||
:key="item.noticeId"
|
||||
class="notice-item"
|
||||
:class="{ 'is-read': noticeIsRead(item) }"
|
||||
@click="handleNoticeRead(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="!noticeIsRead(item)" type="danger" size="small" effect="dark" class="unread-tag">未读</el-tag>
|
||||
</div>
|
||||
<div class="notice-item-time">{{ noticeFormatTime(item.createTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<!-- 帮助ä¸å¿ƒæŒ‰é’® -->
|
||||
<template #reference>
|
||||
<div class="left-action-item">
|
||||
<el-badge :value="noticeStore.unreadCount" :hidden="noticeStore.unreadCount === 0" :max="99">
|
||||
<el-icon><Notification /></el-icon>
|
||||
</el-badge>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
<!-- 帮助中心按钮 -->
|
||||
<el-tooltip
|
||||
content="帮助ä¸å¿ƒ"
|
||||
content="帮助中心"
|
||||
placement="bottom"
|
||||
>
|
||||
<div
|
||||
@@ -93,19 +133,19 @@
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<router-link to="/user/profile">
|
||||
<el-dropdown-item>个人ä¸å¿ƒ</el-dropdown-item>
|
||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item
|
||||
divided
|
||||
command="lockScreen"
|
||||
>
|
||||
<span>é”定å±å¹•</span>
|
||||
<span>锁定屏幕</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
divided
|
||||
command="logout"
|
||||
>
|
||||
<span>退出登录</span>
|
||||
<span>退出登录</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -138,7 +178,7 @@
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
title="切æ¢ç§‘室"
|
||||
title="切换科室"
|
||||
width="400px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@@ -161,147 +201,152 @@
|
||||
type="primary"
|
||||
@click="submit"
|
||||
>
|
||||
确定
|
||||
确定
|
||||
</el-button>
|
||||
<el-button @click="showDialog = false">
|
||||
å–æ¶ˆ
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 公告/é€šçŸ¥é¢æ¿ -->
|
||||
<NoticePanel
|
||||
ref="noticePanelRef"
|
||||
@update-unread-count="updateUnreadCount"
|
||||
/>
|
||||
|
||||
<DetailView ref="detailViewRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, computed} from 'vue';
|
||||
import {ElMessageBox} from 'element-plus';
|
||||
import {Fold, Expand, Bell} from '@element-plus/icons-vue';
|
||||
import HeaderSearch from '@/components/HeaderSearch';
|
||||
import NoticePanel from '@/components/NoticePanel';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useLockStore from '@/store/modules/lock';
|
||||
import {getOrg, switchOrg} from '@/api/login';
|
||||
import {getUnreadCount} from '@/api/system/notice';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {Fold, Expand} from '@element-plus/icons-vue';
|
||||
import {Help, Setting} from "@element-plus/icons-vue";
|
||||
import HeaderSearch from '@/components/HeaderSearch/index.vue';
|
||||
import DetailView from '@/layout/components/HeaderNotice/DetailView.vue'
|
||||
import useNoticeStore from '@/store/modules/notice'
|
||||
import { Notification } from '@element-plus/icons-vue'
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import useLockStore from '@/store/modules/lock';
|
||||
import usePermissionStore from '@/store/modules/permission';
|
||||
import {getOrg, switchOrg} from '@/api/login';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore()
|
||||
const userStore = useUserStore();
|
||||
const lockStore = useLockStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const orgOptions = ref([]);
|
||||
const showDialog = ref(false);
|
||||
const orgId = ref('');
|
||||
const noticePanelRef = ref(null);
|
||||
const unreadCount = ref(0);
|
||||
const {proxy} = getCurrentInstance();
|
||||
|
||||
const sidebar = computed(() => appStore.sidebar);
|
||||
|
||||
// åŠ è½½æœªè¯»æ•°é‡
|
||||
function loadUnreadCount() {
|
||||
getUnreadCount().then(res => {
|
||||
unreadCount.value = res.data || 0;
|
||||
}).catch(() => {
|
||||
unreadCount.value = 0;
|
||||
});
|
||||
}
|
||||
const showDialog = ref(false);
|
||||
const orgId = ref('');
|
||||
const orgOptions = ref([]);
|
||||
|
||||
// 更新未读数é‡
|
||||
function updateUnreadCount() {
|
||||
loadUnreadCount();
|
||||
}
|
||||
|
||||
// 切æ¢ä¾§è¾¹æ
|
||||
// 切换侧边栏
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSideBar();
|
||||
}
|
||||
|
||||
function loadOrgList() {
|
||||
getOrg().then((res) => {
|
||||
orgOptions.value = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOrgList();
|
||||
loadUnreadCount();
|
||||
});
|
||||
|
||||
function handleOrgSwitch(selectedOrgId) {
|
||||
if (selectedOrgId === userStore.orgId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOrg = orgOptions.value.find((item) => item.orgId === selectedOrgId);
|
||||
const orgName = selectedOrg ? selectedOrg.orgName : '该科室';
|
||||
|
||||
ElMessageBox.confirm(`确定è¦åˆ‡æ¢åˆ°ç§‘室"${orgName}"å—?`, '切æ¢ç§‘室', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: 'å–æ¶ˆ',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
orgId.value = selectedOrgId;
|
||||
switchOrg(selectedOrgId).then((res) => {
|
||||
if (res.code === 200) {
|
||||
userStore.logOut().then(() => {
|
||||
location.href = '/index';
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleCommand(command) {
|
||||
switch (command) {
|
||||
case 'setLayout':
|
||||
setLayout();
|
||||
break;
|
||||
case 'lockScreen':
|
||||
lockScreen()
|
||||
break
|
||||
lockStore.lockScreen(route.path);
|
||||
router.push('/lock');
|
||||
break;
|
||||
case 'logout':
|
||||
logout();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function lockScreen() {
|
||||
const currentPath = router.currentRoute.value.path
|
||||
lockStore.lockScreen(currentPath)
|
||||
router.push('/lock')
|
||||
async function loadOrgList() {
|
||||
try {
|
||||
const res = await getOrg();
|
||||
orgOptions.value = res.data || [];
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
ElMessageBox.confirm('确定注销并退出系统å—?', 'æç¤º', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: 'å–æ¶ˆ',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
userStore.logOut().then(() => {
|
||||
location.href = '/index';
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
async function handleOrgSwitch(val) {
|
||||
const selectedOrg = orgOptions.value.find(item => item.orgId === val);
|
||||
const orgName = selectedOrg ? selectedOrg.orgName : '该科室';
|
||||
ElMessageBox.confirm(`确定要切换到科室"${orgName}"吗?`, '切换科室', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await switchOrg(val);
|
||||
permissionStore.setRoutes([]);
|
||||
router.replace({path: '/redirect' + route.path});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function submit() {
|
||||
switchOrg(orgId.value).then((res) => {
|
||||
if (res.code === 200) {
|
||||
userStore.logOut().then(() => {
|
||||
location.href = '/index';
|
||||
});
|
||||
}
|
||||
});
|
||||
handleOrgSwitch(orgId.value);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userStore.logOut().then(() => {
|
||||
router.push('/login');
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function goToHelpCenter() {
|
||||
router.push('/help');
|
||||
}
|
||||
|
||||
|
||||
const noticeStore = useNoticeStore()
|
||||
const noticeVisible = ref(false)
|
||||
const noticeTab = ref('all')
|
||||
const detailViewRef = ref(null)
|
||||
|
||||
const noticeFilteredList = computed(() => {
|
||||
const list = noticeStore.noticeList
|
||||
if (noticeTab.value === 'notice') return list.filter(n => n.noticeType === '1')
|
||||
if (noticeTab.value === 'announce') return list.filter(n => n.noticeType === '2')
|
||||
return list
|
||||
})
|
||||
const noticeUnread = computed(() => noticeStore.noticeList.filter(n => n.noticeType === '1' && !noticeIsRead(n)).length)
|
||||
const announceUnread = computed(() => noticeStore.noticeList.filter(n => n.noticeType === '2' && !noticeIsRead(n)).length)
|
||||
|
||||
function noticeIsRead(item) {
|
||||
return item.isRead || item.readFlag === '1' || noticeStore.readIds.has(item.noticeId)
|
||||
}
|
||||
function noticeFormatTime(time) {
|
||||
if (!time) return ''
|
||||
const d = new Date(time), now = new Date(), 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 handleNoticeRead(item) {
|
||||
if (!noticeIsRead(item)) noticeStore.markRead(item.noticeId)
|
||||
detailViewRef.value?.open(item)
|
||||
}
|
||||
function handleNoticeReadAll() {
|
||||
noticeStore.markAllRead()
|
||||
}
|
||||
|
||||
const emits = defineEmits(['setLayout']);
|
||||
@@ -309,53 +354,43 @@ function setLayout() {
|
||||
emits('setLayout');
|
||||
}
|
||||
|
||||
// 打开公告/é€šçŸ¥é¢æ¿
|
||||
function openNoticePanel() {
|
||||
if (noticePanelRef.value) {
|
||||
noticePanelRef.value.open();
|
||||
}
|
||||
}
|
||||
|
||||
function goToHelpCenter() {
|
||||
window.open(window.location.origin + '/help-center/vuepress-theme-vdoing-doc/docs/.vuepress/dist/pages/520e67/index.html', '_blank');
|
||||
}
|
||||
onMounted(() => {
|
||||
loadOrgList();
|
||||
noticeStore.fetchNotices();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
height: 50px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
.left-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.hamburger-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.hamburger {
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
padding: 0 15px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
color: #5a5e66;
|
||||
|
||||
&:hover {
|
||||
background-color: #f6f6f6;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -612,3 +647,118 @@ function goToHelpCenter() {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 通知弹窗样式 */
|
||||
.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 {
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.el-tabs__nav-wrap::after {
|
||||
display: none;
|
||||
}
|
||||
.el-tabs__item {
|
||||
padding: 0 12px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.tab-badge {
|
||||
margin-left: 4px;
|
||||
.el-badge__content { font-size: 10px; }
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -134,7 +134,7 @@ const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
const showSettings = ref(false)
|
||||
const navType = ref(settingsStore.navType)
|
||||
const navType = ref(settingsStore.topNav)
|
||||
const theme = ref(settingsStore.theme)
|
||||
const sideTheme = ref(settingsStore.sideTheme)
|
||||
const tagsViewPersist = ref(settingsStore.tagsViewPersist)
|
||||
@@ -162,7 +162,7 @@ function handleTheme(val) {
|
||||
}
|
||||
|
||||
function handleNavType(val) {
|
||||
settingsStore.navType = val
|
||||
settingsStore.topNav = val
|
||||
navType.value = val
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ function saveSetting() {
|
||||
proxy.$cache.local.remove('tags-view-visited')
|
||||
}
|
||||
let layoutSetting = {
|
||||
"navType": storeSettings.value.navType,
|
||||
"topNav": storeSettings.value.topNav,
|
||||
"tagsView": storeSettings.value.tagsView,
|
||||
"tagsIcon": storeSettings.value.tagsIcon,
|
||||
"tagsViewStyle": storeSettings.value.tagsViewStyle,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div
|
||||
id="tags-view-container"
|
||||
class="tags-view-container"
|
||||
:class="{ 'chrome-style': tagsViewStyle === 'chrome' }"
|
||||
>
|
||||
<scroll-pane
|
||||
ref="scrollPaneRef"
|
||||
@@ -88,6 +89,7 @@ const router = useRouter();
|
||||
const visitedViews = computed(() => useTagsViewStore().visitedViews);
|
||||
const routes = computed(() => usePermissionStore().routes);
|
||||
const theme = computed(() => useSettingsStore().theme);
|
||||
const tagsViewStyle = computed(() => useSettingsStore().tagsViewStyle);
|
||||
|
||||
watch(route, () => {
|
||||
addTags();
|
||||
@@ -407,6 +409,27 @@ function handleScroll() {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.chrome-style {
|
||||
background: #f1f3f4;
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
border: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: #dadce0;
|
||||
color: #5f6368;
|
||||
margin-left: -1px;
|
||||
padding: 0 16px;
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #202124;
|
||||
border-color: transparent;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -435,5 +458,26 @@ function handleScroll() {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.chrome-style {
|
||||
background: #f1f3f4;
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
border: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: #dadce0;
|
||||
color: #5f6368;
|
||||
margin-left: -1px;
|
||||
padding: 0 16px;
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #202124;
|
||||
border-color: transparent;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,7 @@
|
||||
<navbar @set-layout="setLayout" />
|
||||
<!-- 内容区 -->
|
||||
<div
|
||||
:class="{ 'hasTagsView': needTagsView }"
|
||||
:class="{ 'hasTagsView': needTagsView, 'sidebar-hidden': sidebar.hide }"
|
||||
class="content-wrapper"
|
||||
>
|
||||
<!-- 标签栏 -->
|
||||
@@ -138,11 +138,19 @@ defineExpose({
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
left: 200px;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
width: calc(100% - 200px);
|
||||
padding: 0 15px;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar-hidden {
|
||||
.fixed-header {
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarHide {
|
||||
|
||||
@@ -454,19 +454,9 @@ export const dynamicRoutes = [
|
||||
}
|
||||
];
|
||||
|
||||
// 合并常量路由和动态路由,确保所有路由都能被访问
|
||||
const allRoutes = [...constantRoutes, ...dynamicRoutes];
|
||||
|
||||
// 添加404路由到所有路由的最后
|
||||
allRoutes.push({
|
||||
path: "/:pathMatch(.*)*",
|
||||
component: () => import('@/views/error/404'),
|
||||
hidden: true
|
||||
});
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: allRoutes,
|
||||
routes: constantRoutes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
@@ -476,4 +466,17 @@ const router = createRouter({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// 动态路由加载完成后再添加 404 catch-all(Vue Router 4 要求)
|
||||
export function addNotFoundRoute() {
|
||||
if (!router.hasRoute('not-found')) {
|
||||
router.addRoute({
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
component: () => import('@/views/error/404'),
|
||||
hidden: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
105
openhis-ui-vue3/src/store/modules/notice.js
Normal file
105
openhis-ui-vue3/src/store/modules/notice.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { getUnreadCount, getUserNotices, markAsRead, markAllAsRead, listNoticeTop } from '@/api/system/notice'
|
||||
|
||||
const useNoticeStore = defineStore('notice', {
|
||||
state: () => ({
|
||||
unreadCount: 0,
|
||||
noticeList: [],
|
||||
readIds: new Set(),
|
||||
loaded: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
unreadList(state) {
|
||||
return state.noticeList.filter(n => !state.readIds.has(n.noticeId))
|
||||
},
|
||||
noticeTypeList(state) {
|
||||
return state.noticeList.filter(n => n.noticeType === '2')
|
||||
},
|
||||
informTypeList(state) {
|
||||
return state.noticeList.filter(n => n.noticeType === '1')
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 获取未读数量
|
||||
async fetchUnreadCount() {
|
||||
try {
|
||||
const res = await getUnreadCount()
|
||||
this.unreadCount = res.data || 0
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 获取通知列表
|
||||
async fetchNotices() {
|
||||
try {
|
||||
const res = await getUserNotices()
|
||||
this.noticeList = res.data || []
|
||||
this.loaded = true
|
||||
// 计算未读数
|
||||
this.unreadCount = this.noticeList.filter(n => !n.readFlag || n.readFlag === '0').length
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 获取顶部通知列表
|
||||
async fetchTopNotices(query) {
|
||||
try {
|
||||
const res = await listNoticeTop(query)
|
||||
this.noticeList = res.data || []
|
||||
this.loaded = true
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 标记单条已读
|
||||
async markRead(noticeId) {
|
||||
try {
|
||||
await markAsRead(noticeId)
|
||||
this.readIds.add(noticeId)
|
||||
// 更新列表中的已读状态
|
||||
const item = this.noticeList.find(n => n.noticeId === noticeId)
|
||||
if (item) {
|
||||
item.isRead = true
|
||||
item.readFlag = '1'
|
||||
}
|
||||
this.unreadCount = Math.max(0, this.unreadCount - 1)
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 全部标记已读
|
||||
async markAllRead() {
|
||||
try {
|
||||
const unreadIds = this.noticeList
|
||||
.filter(n => !this.readIds.has(n.noticeId))
|
||||
.map(n => n.noticeId)
|
||||
if (unreadIds.length > 0) {
|
||||
await markAllAsRead(unreadIds)
|
||||
unreadIds.forEach(id => this.readIds.add(id))
|
||||
this.noticeList.forEach(n => {
|
||||
n.isRead = true
|
||||
n.readFlag = '1'
|
||||
})
|
||||
this.unreadCount = 0
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 重置状态
|
||||
reset() {
|
||||
this.unreadCount = 0
|
||||
this.noticeList = []
|
||||
this.readIds = new Set()
|
||||
this.loaded = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default useNoticeStore
|
||||
@@ -1,5 +1,5 @@
|
||||
import auth from '@/plugins/auth'
|
||||
import router, {constantRoutes, dynamicRoutes} from '@/router'
|
||||
import router, {constantRoutes, dynamicRoutes, addNotFoundRoute} from '@/router'
|
||||
import {getRouters} from '@/api/menu'
|
||||
import Layout from '@/layout/index'
|
||||
import ParentView from '@/components/ParentView'
|
||||
@@ -44,6 +44,7 @@ const usePermissionStore = defineStore(
|
||||
const defaultRoutes = filterAsyncRouter(defaultData)
|
||||
const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
|
||||
asyncRoutes.forEach(route => { router.addRoute(route) })
|
||||
addNotFoundRoute()
|
||||
this.setRoutes(rewriteRoutes)
|
||||
this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
|
||||
this.setDefaultRoutes(sidebarRoutes)
|
||||
|
||||
@@ -2,7 +2,7 @@ import defaultSettings from '@/settings'
|
||||
import {useDynamicTitle} from '@/utils/dynamicTitle'
|
||||
|
||||
const {
|
||||
sideTheme, showSettings, navType: topNav, tagsView, tagsViewPersist,
|
||||
sideTheme, showSettings, topNav, tagsView, tagsViewPersist,
|
||||
tagsIcon, tagsViewStyle, fixedHeader, sidebarLogo, dynamicTitle,
|
||||
footerVisible, footerContent
|
||||
} = defaultSettings
|
||||
|
||||
Reference in New Issue
Block a user