342 lines
7.6 KiB
Vue
Executable File
342 lines
7.6 KiB
Vue
Executable File
<template>
|
||
<el-drawer
|
||
v-model="noticeVisible"
|
||
title="公告/通知"
|
||
direction="rtl"
|
||
size="400px"
|
||
destroy-on-close
|
||
>
|
||
<el-empty
|
||
v-if="noticeList.length === 0"
|
||
description="暂无公告/通知"
|
||
/>
|
||
<div
|
||
v-else
|
||
class="notice-list"
|
||
>
|
||
<div
|
||
v-for="item in noticeList"
|
||
:key="item.noticeId"
|
||
class="notice-item"
|
||
:class="{ 'is-read': isRead(item.noticeId), 'unread': !isRead(item.noticeId) }"
|
||
@click="viewDetail(item)"
|
||
>
|
||
<div class="notice-title">
|
||
<span
|
||
v-if="!isRead(item.noticeId)"
|
||
class="unread-dot"
|
||
/>
|
||
{{ item.noticeTitle }}
|
||
</div>
|
||
<div class="notice-info">
|
||
<span class="notice-type">
|
||
<el-tag
|
||
:type="getNoticeTypeTagType(item.noticeType)"
|
||
size="small"
|
||
>
|
||
{{ getNoticeTypeText(item.noticeType) }}
|
||
</el-tag>
|
||
</span>
|
||
<span
|
||
v-if="item.priority"
|
||
class="notice-priority"
|
||
>
|
||
<el-tag
|
||
:type="getPriorityTagType(item.priority)"
|
||
size="small"
|
||
effect="plain"
|
||
>
|
||
{{ getPriorityText(item.priority) }}
|
||
</el-tag>
|
||
</span>
|
||
<span class="notice-time">{{ parseTime(item.createTime, '{y}-{m}-{d}') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 公告/通知详情对话框 -->
|
||
<el-dialog
|
||
v-model="detailVisible"
|
||
:title="currentNotice.noticeTitle"
|
||
width="800px"
|
||
teleported
|
||
>
|
||
<div class="notice-detail">
|
||
<div class="detail-header">
|
||
<div class="detail-type">
|
||
<el-tag
|
||
:type="getNoticeTypeTagType(currentNotice.noticeType)"
|
||
size="small"
|
||
>
|
||
{{ getNoticeTypeText(currentNotice.noticeType) }}
|
||
</el-tag>
|
||
<el-tag
|
||
:type="getPriorityTagType(currentNotice.priority)"
|
||
size="small"
|
||
effect="plain"
|
||
style="margin-left: 8px;"
|
||
>
|
||
{{ getPriorityText(currentNotice.priority) }}
|
||
</el-tag>
|
||
</div>
|
||
<span class="detail-time">{{ parseTime(currentNotice.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
|
||
</div>
|
||
<div
|
||
class="detail-content"
|
||
>{{ safeContent }}</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="detailVisible = false">
|
||
关闭
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {ref} from 'vue'
|
||
import {getReadNoticeIds, getUserNotices, markAsRead} from '@/api/system/notice'
|
||
|
||
const emit = defineEmits(['updateUnreadCount'])
|
||
|
||
const noticeVisible = ref(false)
|
||
const detailVisible = ref(false)
|
||
const noticeList = ref([])
|
||
const currentNotice = ref({})
|
||
const readNoticeIds = ref(new Set())
|
||
const safeContent = computed(() => {
|
||
const content = currentNotice.value && currentNotice.value.noticeContent
|
||
return content ? String(content).replace(/<[^>]*>/g, '') : ''
|
||
})
|
||
|
||
// 打开公告/通知面板
|
||
function open() {
|
||
noticeVisible.value = true
|
||
loadNotices()
|
||
loadReadNoticeIds()
|
||
}
|
||
|
||
// 加载已读公告ID列表
|
||
function loadReadNoticeIds() {
|
||
getReadNoticeIds().then(response => {
|
||
const ids = response.data || []
|
||
readNoticeIds.value = new Set(ids)
|
||
// 同步到 localStorage
|
||
localStorage.setItem('readNoticeIds', JSON.stringify(ids))
|
||
}).catch(() => {
|
||
// 接口调用失败时从 localStorage 读取
|
||
const readIds = localStorage.getItem('readNoticeIds')
|
||
if (readIds) {
|
||
try {
|
||
const ids = JSON.parse(readIds)
|
||
readNoticeIds.value = new Set(ids)
|
||
} catch (e) {
|
||
console.error('解析已读ID失败', e)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 排序:未读的排前面,已读的排后面,同状态按优先级和时间排序
|
||
function sortNoticeList(list) {
|
||
return list.sort((a, b) => {
|
||
const aRead = isRead(a.noticeId)
|
||
const bRead = isRead(b.noticeId)
|
||
|
||
// 未读排在前面
|
||
if (aRead !== bRead) {
|
||
return aRead ? 1 : -1
|
||
}
|
||
|
||
// 同状态按优先级排序(1高 2中 3低)
|
||
const priorityA = a.priority || '3'
|
||
const priorityB = b.priority || '3'
|
||
if (priorityA !== priorityB) {
|
||
return priorityA.localeCompare(priorityB)
|
||
}
|
||
|
||
// 同优先级按创建时间倒序(最新的在前)
|
||
return new Date(b.createTime) - new Date(a.createTime)
|
||
})
|
||
}
|
||
|
||
// 加载公告和通知(统一从一个接口获取)
|
||
function loadNotices() {
|
||
getUserNotices().then(response => {
|
||
let list = response.data || []
|
||
noticeList.value = sortNoticeList(list)
|
||
})
|
||
}
|
||
|
||
// 获取公告类型标签类型
|
||
// noticeType: 1=通知, 2=公告
|
||
function getNoticeTypeTagType(type) {
|
||
const typeMap = {
|
||
'1': 'primary', // 通知
|
||
'2': 'success' // 公告
|
||
}
|
||
return typeMap[type] || 'info'
|
||
}
|
||
|
||
// 获取公告类型文本
|
||
function getNoticeTypeText(type) {
|
||
const textMap = {
|
||
'1': '通知',
|
||
'2': '公告'
|
||
}
|
||
return textMap[type] || '公告'
|
||
}
|
||
|
||
// 获取优先级标签类型
|
||
// priority: 1=高, 2=中, 3=低
|
||
function getPriorityTagType(priority) {
|
||
const typeMap = {
|
||
'1': 'danger', // 高优先级 - 红色
|
||
'2': 'warning', // 中优先级 - 橙色
|
||
'3': 'info' // 低优先级 - 灰色
|
||
}
|
||
return typeMap[priority] || 'info'
|
||
}
|
||
|
||
// 获取优先级文本
|
||
function getPriorityText(priority) {
|
||
const textMap = {
|
||
'1': '高',
|
||
'2': '中',
|
||
'3': '低'
|
||
}
|
||
return textMap[priority] || '中'
|
||
}
|
||
|
||
// 查看详情
|
||
function viewDetail(item) {
|
||
currentNotice.value = item
|
||
detailVisible.value = true
|
||
|
||
// 标记为已读
|
||
if (!readNoticeIds.value.has(item.noticeId)) {
|
||
markAsRead(item.noticeId).then(() => {
|
||
readNoticeIds.value.add(item.noticeId)
|
||
// 保存到 localStorage
|
||
saveReadNoticeIds()
|
||
emit('updateUnreadCount')
|
||
})
|
||
}
|
||
}
|
||
|
||
// 保存已读公告ID列表
|
||
function saveReadNoticeIds() {
|
||
const ids = Array.from(readNoticeIds.value)
|
||
localStorage.setItem('readNoticeIds', JSON.stringify(ids))
|
||
}
|
||
|
||
// 检查是否已读
|
||
function isRead(noticeId) {
|
||
return readNoticeIds.value.has(noticeId)
|
||
}
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
open,
|
||
isRead,
|
||
readNoticeIds
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.notice-list {
|
||
max-height: calc(100vh - 200px);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.notice-item {
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #EBEEF5;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
|
||
&:hover {
|
||
background-color: #F5F7FA;
|
||
}
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&.is-read {
|
||
.notice-title {
|
||
color: #64748B;
|
||
}
|
||
}
|
||
|
||
&.unread {
|
||
background-color: #fffbe6;
|
||
}
|
||
}
|
||
|
||
.notice-title {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
line-height: 1.4;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.unread-dot {
|
||
display: inline-block;
|
||
width: 6px;
|
||
height: 6px;
|
||
background-color: #EF4444;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.notice-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #64748B;
|
||
gap: 8px;
|
||
|
||
.notice-type,
|
||
.notice-priority {
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.detail-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid #EBEEF5;
|
||
margin-bottom: 16px;
|
||
|
||
.detail-type {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.detail-time {
|
||
font-size: 12px;
|
||
color: #64748B;
|
||
}
|
||
|
||
.detail-content {
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: #303133;
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
:deep(.el-drawer__body) {
|
||
padding: 0 20px;
|
||
}
|
||
</style>
|