feat(notice): 添加公告详情查看功能并优化通知面板界面

- 在后端控制器中新增公开接口获取公告详情,支持状态检查和已读标记
- 在前端API模块中添加获取公共公告详情的方法
- 更新通知面板组件导入新的公共公告API方法
- 重构头部通知组件实现内联查看详情模式,移除独立详情弹窗
- 优化通知面板UI界面,调整布局样式和交互体验
- 将原有的Navbar中的通知弹窗替换为新的HeaderNotice组件
- 移除旧的通知相关代码和样式,精简组件结构
This commit is contained in:
2026-06-04 14:13:32 +08:00
parent e84455da51
commit b9856d3ce6
7 changed files with 233 additions and 479 deletions

View File

@@ -102,6 +102,30 @@ public class SysNoticeController extends BaseController {
return success(list);
}
/**
* 获取公告/通知详情(公开接口,普通用户可用)
* 仅返回已发布且状态正常的公告
*/
@GetMapping("/public/{noticeId}")
public AjaxResult getPublicNotice(@PathVariable Long noticeId) {
SysNotice notice = noticeService.selectNoticeById(noticeId);
if (notice == null) {
return error("公告不存在");
}
// 只允许查看已发布且状态正常的公告
if (!"1".equals(notice.getPublishStatus()) || !"0".equals(notice.getStatus())) {
return error("该公告未发布或已关闭");
}
// 标注当前用户是否已读
LoginUser loginUser = getLoginUser();
if (loginUser != null) {
List<Long> readIds = noticeReadService.selectReadNoticeIdsByUserId(loginUser.getUser().getUserId());
notice.setIsRead(readIds.contains(noticeId));
}
return success(notice);
}
/**
* 获取用户未读公告/通知数量(公开接口)
*/

View File

@@ -26,6 +26,15 @@ export function getUserNotices() {
})
}
// 获取公告/通知详情(公开接口,普通用户可用)
export function getPublicNotice(noticeId) {
return request({
url: '/system/notice/public/' + noticeId,
method: 'get'
})
}
// 查询公告详细
export function getNotice(noticeId) {
return request({

View File

@@ -140,7 +140,7 @@
<script setup>
import { ref, computed, defineEmits } from 'vue'
import { getUserNotices, markAsRead, markAllAsRead as markAllReadApi } from '@/api/system/notice'
import { getUserNotices, getPublicNotice, markAsRead, markAllAsRead as markAllReadApi } from '@/api/system/notice'
import { Bell, Warning, InfoFilled, ArrowRight, CircleCheck } from '@element-plus/icons-vue'
const emit = defineEmits(['updateUnreadCount'])

View File

@@ -129,7 +129,7 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { getUserNotices, markAsRead, markAllAsRead as markAllReadApi } from '@/api/system/notice'
import { getUserNotices, getPublicNotice, markAsRead, markAllAsRead as markAllReadApi } from '@/api/system/notice'
import { Bell, Warning, InfoFilled, ArrowRight, CircleCheck } from '@element-plus/icons-vue'
// 监听全局触发公告弹窗事件

View File

@@ -53,7 +53,7 @@
</template>
<script setup>
import { getNotice } from '@/api/system/notice'
import { getPublicNotice } from '@/api/system/notice'
const visible = ref(false)
const loading = ref(false)
@@ -91,7 +91,7 @@ function open(payload) {
}
loading.value = true
detail.value = null
getNotice(id).then(res => {
getPublicNotice(id).then(res => {
detail.value = res.data
}).catch(() => {
detail.value = null

View File

@@ -1,360 +1,285 @@
<template>
<div>
<el-popover
ref="noticePopover"
placement="bottom-end"
:width="360"
:width="380"
trigger="click"
v-model:visible="noticeVisible"
popper-class="notice-popover"
:show-arrow="false"
:offset="8"
>
<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 v-if="!activeNotice" class="notice-popover-content">
<div class="notice-header">
<span class="notice-header-title">消息中心</span>
<span class="notice-header-action" @click="handleReadAll">全部已读</span>
</div>
</template>
<el-tabs v-model="activeTab" 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="380px" 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="openDetail(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-desc" v-if="item.noticeContent">{{ stripHtml(item.noticeContent) }}</div>
<div class="notice-item-time">{{ formatTime(item.createTime) }}</div>
</div>
<el-icon class="notice-item-arrow"><ArrowRight /></el-icon>
</div>
</el-scrollbar>
</div>
<!-- 详情视图 -->
<div v-else class="notice-detail-content">
<div class="detail-header">
<div class="detail-back" @click="closeDetail">
<el-icon><ArrowLeft /></el-icon>
<span>返回</span>
</div>
<el-tag v-if="!isRead(activeNotice)" type="danger" size="small" effect="dark">未读</el-tag>
</div>
<div class="detail-type-bar">
<span v-if="activeNotice.noticeType === '1'" class="detail-type-tag type-notify">
<el-icon><Bell /></el-icon> 通知
</span>
<span v-else class="detail-type-tag type-announce">
<el-icon><Notification /></el-icon> 公告
</span>
<span class="detail-priority" :class="'priority-' + (activeNotice.priority || '2')">
{{ priorityText(activeNotice.priority) }}
</span>
</div>
<h3 class="detail-title">{{ activeNotice.noticeTitle }}</h3>
<div class="detail-meta">
<span><el-icon><User /></el-icon> {{ activeNotice.createBy || '系统' }}</span>
<span><el-icon><Clock /></el-icon> {{ activeNotice.createTime }}</span>
</div>
<el-scrollbar max-height="320px" class="detail-body-scroll">
<div class="detail-body" v-html="activeNotice.noticeContent" />
</el-scrollbar>
</div>
<template #reference>
<div class="left-action-item">
<div class="notice-trigger-icon">
<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'
import { getPublicNotice } from '@/api/system/notice'
import { Bell, Notification, ArrowRight, ArrowLeft, User, Clock } from '@element-plus/icons-vue'
const noticeStore = useNoticeStore()
const noticeVisible = ref(false)
const activeTab = ref('all')
const detailViewRef = ref(null)
const activeNotice = 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')
}
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
})
const noticeUnread = computed(() => noticeStore.noticeList.filter(n => n.noticeType === '1' && !isRead(n)).length)
const announceUnread = computed(() => 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
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())}`
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 stripHtml(html) {
if (!html) return ''
return html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim().substring(0, 80)
}
function priorityText(p) {
return { '1': '高优先级', '2': '中优先级', '3': '低优先级' }[p] || '中优先级'
}
function openDetail(item) {
if (!isRead(item)) noticeStore.markRead(item.noticeId)
// 先用列表数据展示,再请求完整内容
activeNotice.value = { ...item }
getPublicNotice(item.noticeId).then(res => {
if (res.data) activeNotice.value = res.data
}).catch(() => {})
}
function closeDetail() {
activeNotice.value = null
}
// 全部已读
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-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;
}
}
: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-empty { padding: 30px 0; }
.notice-item {
display: flex;
align-items: flex-start;
gap: 12px;
gap: 10px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
transition: background 0.15s;
border-bottom: 1px solid #f5f5f5;
&:hover {
background: #f7f8fa;
}
&:last-child {
border-bottom: none;
}
&.is-read {
opacity: 0.55;
.notice-item-title span {
color: #999;
}
}
&:hover { background: #f7f8fa; }
&:last-child { border-bottom: none; }
&.is-read { opacity: 0.5; }
.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;
}
width: 34px; height: 34px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
.icon-notice { background: #E0F2FE; color: #0284C7; width: 34px; height: 34px; border-radius: 50%; font-size: 15px; }
.icon-announce { background: #ECFDF5; color: #059669; width: 34px; height: 34px; border-radius: 50%; font-size: 15px; }
}
.notice-item-body {
flex: 1;
min-width: 0;
}
.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;
display: flex; align-items: center; gap: 6px; margin-bottom: 3px;
span { flex: 1; font-size: 13px; font-weight: 500; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.unread-tag { flex-shrink: 0; }
}
.notice-item-desc { font-size: 12px; color: #999; line-height: 1.4; margin-bottom: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notice-item-time { font-size: 11px; color: #bbb; }
.notice-item-arrow { flex-shrink: 0; color: #ccc; font-size: 12px; margin-top: 10px; }
}
.notice-footer {
/* 详情视图 */
.notice-detail-content {
display: flex;
flex-direction: column;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
.detail-back {
display: flex; align-items: center; gap: 4px;
font-size: 13px; color: #3B82F6; cursor: pointer;
&:hover { color: #2563EB; }
.el-icon { font-size: 14px; }
}
}
.detail-type-bar {
display: flex; align-items: center; gap: 8px;
padding: 12px 16px 0;
.detail-type-tag {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 10px; border-radius: 3px; font-size: 12px; font-weight: 500;
&.type-notify { background: #E0F2FE; color: #0284C7; }
&.type-announce { background: #ECFDF5; color: #059669; }
}
.detail-priority {
font-size: 12px; padding: 2px 8px; border-radius: 3px;
&.priority-1 { background: #FEF2F2; color: #DC2626; }
&.priority-2 { background: #FFFBEB; color: #D97706; }
&.priority-3 { background: #F3F4F6; color: #6B7280; }
}
}
.detail-title {
font-size: 16px; font-weight: 600; color: #1a1a1a;
margin: 10px 16px 0; line-height: 1.5;
}
.detail-meta {
display: flex; gap: 16px; padding: 8px 16px 12px;
font-size: 12px; color: #999;
span { display: flex; align-items: center; gap: 4px; }
.el-icon { font-size: 12px; }
}
.detail-body-scroll {
border-top: 1px solid #f0f0f0;
}
.detail-body {
padding: 16px;
font-size: 14px; line-height: 1.8; color: #333;
:deep(img) { max-width: 100%; border-radius: 4px; margin: 8px 0; }
:deep(p) { margin: 8px 0; }
:deep(a) { color: #3B82F6; }
}
/* 触发器图标 */
.notice-trigger-icon {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px;
border-top: 1px solid #f0f0f0;
font-size: 13px;
color: #3B82F6;
height: 50px;
padding: 0 10px;
cursor: pointer;
transition: background 0.2s;
color: #606266;
transition: background 0.3s;
&:hover {
background: #f7f8fa;
background-color: #f6f6f6;
color: #3B82F6;
}
.el-icon {
font-size: 18px;
}
}
</style>

View File

@@ -20,63 +20,7 @@
/>
</template>
<!-- 消息通知 -->
<el-popover
placement="bottom-end"
:width="360"
trigger="click"
v-model:visible="noticeVisible"
popper-class="notice-popover"
:show-arrow="false"
>
<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>
<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>
<HeaderNotice />
<!-- 帮助中心按钮 -->
<el-tooltip
content="帮助中心"
@@ -211,7 +155,6 @@
</el-dialog>
<DetailView ref="detailViewRef" />
</div>
</template>
@@ -220,9 +163,7 @@ import {ElMessageBox} from 'element-plus';
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 HeaderNotice from '@/layout/components/HeaderNotice/index.vue';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import useLockStore from '@/store/modules/lock';
@@ -313,40 +254,10 @@ function goToHelpCenter() {
}
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']);
function setLayout() {
@@ -355,7 +266,6 @@ function setLayout() {
onMounted(() => {
loadOrgList();
noticeStore.fetchNotices();
});
</script>
@@ -647,117 +557,3 @@ onMounted(() => {
}
</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>