|
|
@@ -21,37 +21,47 @@
|
|
|
<template #content>
|
|
|
<div class="message-panel">
|
|
|
<a-tabs v-model:activeKey="messageTab" centered>
|
|
|
- <a-tab-pane key="notification" tab="通知">
|
|
|
+ <a-tab-pane key="notification">
|
|
|
+ <template #tab>
|
|
|
+ <a-badge :count="unreadNotificationCount" :offset="[6, -2]" size="small">通知</a-badge>
|
|
|
+ </template>
|
|
|
<div class="message-list">
|
|
|
- <template v-if="notificationList.length > 0">
|
|
|
+ <template v-if="displayNotificationList.length > 0">
|
|
|
<div
|
|
|
- v-for="item in notificationList"
|
|
|
+ v-for="item in displayNotificationList"
|
|
|
:key="item.id"
|
|
|
class="message-item"
|
|
|
+ :class="{ unread: !item.isRead }"
|
|
|
@click="handleNotificationClick(item)"
|
|
|
>
|
|
|
<div class="message-icon" :class="item.type">
|
|
|
<BellOutlined v-if="item.type === 'system'" />
|
|
|
- <MessageOutlined v-else-if="item.type === 'message'" />
|
|
|
<CheckCircleOutlined v-else-if="item.type === 'success'" />
|
|
|
+ <WarningOutlined v-else-if="item.type === 'warning'" />
|
|
|
<InfoCircleOutlined v-else />
|
|
|
</div>
|
|
|
<div class="message-content">
|
|
|
<div class="message-title">{{ item.title }}</div>
|
|
|
+ <div class="message-desc" v-if="item.content">{{ truncateText(item.content, 30) }}</div>
|
|
|
<div class="message-time">{{ item.time }}</div>
|
|
|
</div>
|
|
|
+ <div v-if="!item.isRead" class="unread-dot"></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<a-empty v-else description="暂无通知" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
|
|
</div>
|
|
|
</a-tab-pane>
|
|
|
- <a-tab-pane key="message" tab="消息">
|
|
|
+ <a-tab-pane key="message">
|
|
|
+ <template #tab>
|
|
|
+ <a-badge :count="unreadMessageCount" :offset="[6, -2]" size="small">消息</a-badge>
|
|
|
+ </template>
|
|
|
<div class="message-list">
|
|
|
- <template v-if="messageList.length > 0">
|
|
|
+ <template v-if="displayMessageList.length > 0">
|
|
|
<div
|
|
|
- v-for="item in messageList"
|
|
|
+ v-for="item in displayMessageList"
|
|
|
:key="item.id"
|
|
|
class="message-item"
|
|
|
+ :class="{ unread: !item.isRead }"
|
|
|
@click="handleMessageClick(item)"
|
|
|
>
|
|
|
<a-avatar :src="item.avatar" :size="36">{{ item.sender?.charAt(0) }}</a-avatar>
|
|
|
@@ -60,12 +70,16 @@
|
|
|
<div class="message-desc">{{ item.content }}</div>
|
|
|
<div class="message-time">{{ item.time }}</div>
|
|
|
</div>
|
|
|
+ <div v-if="!item.isRead" class="unread-dot"></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<a-empty v-else description="暂无消息" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
|
|
</div>
|
|
|
</a-tab-pane>
|
|
|
- <a-tab-pane key="todo" tab="待办">
|
|
|
+ <a-tab-pane key="todo">
|
|
|
+ <template #tab>
|
|
|
+ <a-badge :count="todoList.length" :offset="[6, -2]" size="small">待办</a-badge>
|
|
|
+ </template>
|
|
|
<div class="message-list">
|
|
|
<template v-if="todoList.length > 0">
|
|
|
<div
|
|
|
@@ -74,7 +88,7 @@
|
|
|
class="message-item"
|
|
|
@click="handleTodoClick(item)"
|
|
|
>
|
|
|
- <div class="message-icon todo">
|
|
|
+ <div class="message-icon" :class="getPriorityClass(item.priority)">
|
|
|
<ClockCircleOutlined />
|
|
|
</div>
|
|
|
<div class="message-content">
|
|
|
@@ -89,13 +103,13 @@
|
|
|
</a-tab-pane>
|
|
|
</a-tabs>
|
|
|
<div class="message-footer">
|
|
|
- <a-button type="link" @click="clearAll">清空</a-button>
|
|
|
- <a-button type="link" @click="goMessageCenter">查看更多</a-button>
|
|
|
+ <a-button type="link" @click="markAllRead">全部已读</a-button>
|
|
|
+ <a-button type="link" @click="goMore">查看更多</a-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<div class="right-menu-item hover-effect message-center">
|
|
|
- <a-badge :count="unreadCount" :number-style="{ display: unreadCount === 0 ? 'none' : 'block' }" :overflow-count="99">
|
|
|
+ <a-badge :count="totalUnreadCount" :overflow-count="99">
|
|
|
<BellOutlined :style="{ fontSize: '18px' }" />
|
|
|
</a-badge>
|
|
|
</div>
|
|
|
@@ -123,14 +137,27 @@
|
|
|
</template>
|
|
|
</a-dropdown>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 通知详情弹窗 -->
|
|
|
+ <a-modal
|
|
|
+ v-model:open="notificationDetailVisible"
|
|
|
+ :title="currentNotification.title"
|
|
|
+ :footer="null"
|
|
|
+ width="520px"
|
|
|
+ centered
|
|
|
+ destroy-on-close
|
|
|
+ >
|
|
|
+ <div class="notification-detail-content" v-html="currentNotification.content || '暂无内容'"></div>
|
|
|
+ <div class="notification-detail-time">{{ currentNotification.time }}</div>
|
|
|
+ </a-modal>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
-import { useRouter } from 'vue-router'
|
|
|
-import { Modal, Empty } from 'ant-design-vue'
|
|
|
-import { BellOutlined, MessageOutlined, CheckCircleOutlined, InfoCircleOutlined, ClockCircleOutlined } from '@ant-design/icons-vue'
|
|
|
+import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
+import { useRouter, useRoute } from 'vue-router'
|
|
|
+import { Modal, Empty, notification } from 'ant-design-vue'
|
|
|
+import { BellOutlined, MessageOutlined, CheckCircleOutlined, InfoCircleOutlined, ClockCircleOutlined, WarningOutlined } from '@ant-design/icons-vue'
|
|
|
import Breadcrumb from '@/components/Breadcrumb'
|
|
|
import TopNav from '@/components/TopNav'
|
|
|
import Hamburger from '@/components/Hamburger'
|
|
|
@@ -140,6 +167,10 @@ import useAppStore from '@/store/modules/app'
|
|
|
import useUserStore from '@/store/modules/user'
|
|
|
import useSettingsStore from '@/store/modules/settings'
|
|
|
import wsService from '@/utils/websocket'
|
|
|
+import { getMyTodo } from '@/api/system/todo'
|
|
|
+import { getAllNotifications, markNotificationAsRead } from '@/api/system/notification'
|
|
|
+import { getConversationList, markConversationAsRead } from '@/api/system/message'
|
|
|
+import { formatTimeAgo } from '@/utils/formatTime'
|
|
|
|
|
|
const router = useRouter()
|
|
|
const appStore = useAppStore()
|
|
|
@@ -150,69 +181,247 @@ const settingsStore = useSettingsStore()
|
|
|
const messagePopoverVisible = ref(false)
|
|
|
const messageTab = ref('notification')
|
|
|
|
|
|
-// 未读消息数
|
|
|
-const unreadCount = ref(0)
|
|
|
-
|
|
|
-// 通知列表
|
|
|
-const notificationList = ref([
|
|
|
- { id: 1, type: 'system', title: '系统升级通知', time: '10分钟前' },
|
|
|
- { id: 2, type: 'success', title: '您的申请已通过审核', time: '1小时前' },
|
|
|
- { id: 3, type: 'info', title: '新功能上线提醒', time: '2小时前' },
|
|
|
-])
|
|
|
-
|
|
|
-// 消息列表
|
|
|
-const messageList = ref([
|
|
|
- { id: 1, sender: '张三', avatar: '', content: '请问项目进度如何?', time: '5分钟前' },
|
|
|
- { id: 2, sender: '李四', avatar: '', content: '文档已经发送给您了', time: '30分钟前' },
|
|
|
-])
|
|
|
-
|
|
|
-// 待办列表
|
|
|
-const todoList = ref([
|
|
|
- { id: 1, title: '审批流程', description: '有3个待审批事项', time: '待处理' },
|
|
|
- { id: 2, title: '周报提交', description: '本周周报尚未提交', time: '截止今天' },
|
|
|
-])
|
|
|
-
|
|
|
-// 处理通知点击
|
|
|
-function handleNotificationClick(item) {
|
|
|
- console.log('点击通知:', item)
|
|
|
+// 通知详情弹窗
|
|
|
+const notificationDetailVisible = ref(false)
|
|
|
+const currentNotification = ref({
|
|
|
+ title: '',
|
|
|
+ content: '',
|
|
|
+ time: ''
|
|
|
+})
|
|
|
+
|
|
|
+// 最大显示条数
|
|
|
+const MAX_DISPLAY = 5
|
|
|
+
|
|
|
+// 通知列表(原始数据,系统通知)
|
|
|
+const notificationList = ref([])
|
|
|
+
|
|
|
+// 消息列表(原始数据,私聊消息)
|
|
|
+const messageList = ref([])
|
|
|
+
|
|
|
+// 待办列表(只显示待处理的)
|
|
|
+const todoList = ref([])
|
|
|
+
|
|
|
+// 计算未读通知数
|
|
|
+const unreadNotificationCount = computed(() => {
|
|
|
+ return notificationList.value.filter(item => !item.isRead).length
|
|
|
+})
|
|
|
+
|
|
|
+// 计算未读消息数
|
|
|
+const unreadMessageCount = computed(() => {
|
|
|
+ return messageList.value.filter(item => !item.isRead).length
|
|
|
+})
|
|
|
+
|
|
|
+// 总未读数
|
|
|
+const totalUnreadCount = computed(() => {
|
|
|
+ return unreadNotificationCount.value + unreadMessageCount.value + todoList.value.length
|
|
|
+})
|
|
|
+
|
|
|
+// 计算显示的通知列表(未读优先,最多5条,但未读可以超过5条)
|
|
|
+const displayNotificationList = computed(() => {
|
|
|
+ const unread = notificationList.value.filter(item => !item.isRead)
|
|
|
+ const read = notificationList.value.filter(item => item.isRead)
|
|
|
+
|
|
|
+ // 如果未读数量 >= 5,只显示未读
|
|
|
+ if (unread.length >= MAX_DISPLAY) {
|
|
|
+ return unread
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则,未读 + 已读补足到5条
|
|
|
+ const remainingSlots = MAX_DISPLAY - unread.length
|
|
|
+ return [...unread, ...read.slice(0, remainingSlots)]
|
|
|
+})
|
|
|
+
|
|
|
+// 计算显示的消息列表(未读优先,最多5条,但未读可以超过5条)
|
|
|
+const displayMessageList = computed(() => {
|
|
|
+ const unread = messageList.value.filter(item => !item.isRead)
|
|
|
+ const read = messageList.value.filter(item => item.isRead)
|
|
|
+
|
|
|
+ // 如果未读数量 >= 5,只显示未读
|
|
|
+ if (unread.length >= MAX_DISPLAY) {
|
|
|
+ return unread
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则,未读 + 已读补足到5条
|
|
|
+ const remainingSlots = MAX_DISPLAY - unread.length
|
|
|
+ return [...unread, ...read.slice(0, remainingSlots)]
|
|
|
+})
|
|
|
+
|
|
|
+// 获取待办优先级样式
|
|
|
+function getPriorityClass(priority) {
|
|
|
+ const classes = { '0': 'low', '1': 'normal', '2': 'high', '3': 'urgent' }
|
|
|
+ return classes[priority] || 'normal'
|
|
|
+}
|
|
|
+
|
|
|
+// 截断文本
|
|
|
+function truncateText(text, maxLength = 30) {
|
|
|
+ if (!text) return ''
|
|
|
+ // 去除HTML标签
|
|
|
+ const plainText = text.replace(/<[^>]+>/g, '')
|
|
|
+ if (plainText.length <= maxLength) return plainText
|
|
|
+ return plainText.substring(0, maxLength) + '...'
|
|
|
+}
|
|
|
+
|
|
|
+// 显示通知详情弹窗
|
|
|
+function showNotificationDetail(item) {
|
|
|
+ // 先关闭 popover
|
|
|
messagePopoverVisible.value = false
|
|
|
+
|
|
|
+ // 设置当前通知内容
|
|
|
+ currentNotification.value = {
|
|
|
+ title: item.title,
|
|
|
+ content: item.content || '暂无内容',
|
|
|
+ time: item.time
|
|
|
+ }
|
|
|
+
|
|
|
+ // 延迟显示弹窗,确保 popover 已关闭
|
|
|
+ setTimeout(() => {
|
|
|
+ notificationDetailVisible.value = true
|
|
|
+ }, 100)
|
|
|
+}
|
|
|
+
|
|
|
+// 获取待办列表(只获取待处理的)
|
|
|
+function fetchTodoList() {
|
|
|
+ getMyTodo().then(response => {
|
|
|
+ const list = response.rows || response.data || []
|
|
|
+ // 只显示待处理状态的待办
|
|
|
+ todoList.value = list
|
|
|
+ .filter(item => item.status === '0')
|
|
|
+ .map(item => ({
|
|
|
+ id: item.todoId,
|
|
|
+ title: item.title,
|
|
|
+ description: item.content || '',
|
|
|
+ time: item.dueDate ? `截止 ${item.dueDate.substring(0, 10)}` : '待处理',
|
|
|
+ status: item.status,
|
|
|
+ priority: item.priority
|
|
|
+ }))
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+// 获取通知列表(系统通知)
|
|
|
+function fetchNotificationList() {
|
|
|
+ getAllNotifications().then(response => {
|
|
|
+ const list = response.data || response.rows || []
|
|
|
+ notificationList.value = list.map(item => ({
|
|
|
+ id: item.notification_id || item.notificationId || item.id,
|
|
|
+ type: 'system',
|
|
|
+ title: item.title,
|
|
|
+ content: item.content,
|
|
|
+ time: formatTimeAgo(item.create_time || item.createTime),
|
|
|
+ isRead: item.is_read === '1' || item.isRead === '1'
|
|
|
+ }))
|
|
|
+ }).catch(() => {
|
|
|
+ notificationList.value = []
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 获取消息列表(私聊会话)
|
|
|
+function fetchMessageList() {
|
|
|
+ getConversationList().then(response => {
|
|
|
+ const list = response.data || response.rows || []
|
|
|
+ messageList.value = list
|
|
|
+ .filter(item => item.conversation_id !== 'sys_notification') // 排除系统通知会话
|
|
|
+ .map(item => ({
|
|
|
+ id: item.conversation_id || item.conversationId || item.id,
|
|
|
+ sender: item.other_members || item.otherUserName || item.remarkName || '未知用户',
|
|
|
+ avatar: item.avatar || '',
|
|
|
+ content: item.last_message || item.lastMessage || '',
|
|
|
+ time: formatTimeAgo(item.last_message_time || item.lastMessageTime),
|
|
|
+ isRead: (item.unread_count || item.unreadCount || 0) === 0
|
|
|
+ }))
|
|
|
+ }).catch(() => {
|
|
|
+ messageList.value = []
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-// 处理消息点击
|
|
|
+// 处理通知点击 - 标记为已读并显示详情弹窗
|
|
|
+function handleNotificationClick(item) {
|
|
|
+ // 标记为已读
|
|
|
+ if (!item.isRead) {
|
|
|
+ markNotificationAsRead(item.id).then(() => {
|
|
|
+ const index = notificationList.value.findIndex(n => n.id === item.id)
|
|
|
+ if (index !== -1) {
|
|
|
+ notificationList.value[index].isRead = true
|
|
|
+ }
|
|
|
+ }).catch(() => {})
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示详情弹窗
|
|
|
+ showNotificationDetail(item)
|
|
|
+}
|
|
|
+
|
|
|
+// 处理消息点击 - 标记为已读并跳转到对应会话
|
|
|
function handleMessageClick(item) {
|
|
|
- console.log('点击消息:', item)
|
|
|
+ if (!item.isRead) {
|
|
|
+ // 调用API标记已读
|
|
|
+ markConversationAsRead(item.id).then(() => {
|
|
|
+ const index = messageList.value.findIndex(m => m.id === item.id)
|
|
|
+ if (index !== -1) {
|
|
|
+ messageList.value[index].isRead = true
|
|
|
+ }
|
|
|
+ }).catch(() => {})
|
|
|
+ }
|
|
|
messagePopoverVisible.value = false
|
|
|
- router.push('/message')
|
|
|
+ // 跳转到消息页面并传递会话ID,添加时间戳确保路由刷新
|
|
|
+ router.push({
|
|
|
+ path: '/message',
|
|
|
+ query: { conversationId: item.id, t: Date.now() }
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-// 处理待办点击
|
|
|
+// 处理待办点击 - 跳转到待办列表
|
|
|
function handleTodoClick(item) {
|
|
|
- console.log('点击待办:', item)
|
|
|
messagePopoverVisible.value = false
|
|
|
+ router.push('/system/todo')
|
|
|
}
|
|
|
|
|
|
-// 清空消息
|
|
|
-function clearAll() {
|
|
|
+// 全部标记为已读
|
|
|
+function markAllRead() {
|
|
|
if (messageTab.value === 'notification') {
|
|
|
- notificationList.value = []
|
|
|
+ // 批量标记通知为已读
|
|
|
+ const unreadIds = notificationList.value.filter(item => !item.isRead).map(item => item.id)
|
|
|
+ if (unreadIds.length > 0) {
|
|
|
+ // 逐个标记(如果有批量接口可以替换)
|
|
|
+ Promise.all(unreadIds.map(id => markNotificationAsRead(id))).then(() => {
|
|
|
+ notificationList.value.forEach(item => {
|
|
|
+ item.isRead = true
|
|
|
+ })
|
|
|
+ }).catch(() => {})
|
|
|
+ }
|
|
|
} else if (messageTab.value === 'message') {
|
|
|
- messageList.value = []
|
|
|
- } else {
|
|
|
- todoList.value = []
|
|
|
+ // 批量标记消息为已读
|
|
|
+ const unreadIds = messageList.value.filter(item => !item.isRead).map(item => item.id)
|
|
|
+ if (unreadIds.length > 0) {
|
|
|
+ Promise.all(unreadIds.map(id => markConversationAsRead(id))).then(() => {
|
|
|
+ messageList.value.forEach(item => {
|
|
|
+ item.isRead = true
|
|
|
+ })
|
|
|
+ }).catch(() => {})
|
|
|
+ }
|
|
|
}
|
|
|
+ // 待办不需要标记已读
|
|
|
}
|
|
|
|
|
|
-// 跳转消息中心
|
|
|
-function goMessageCenter() {
|
|
|
+// 查看更多
|
|
|
+function goMore() {
|
|
|
messagePopoverVisible.value = false
|
|
|
- router.push('/message')
|
|
|
+ if (messageTab.value === 'todo') {
|
|
|
+ router.push('/system/todo')
|
|
|
+ } else {
|
|
|
+ // 通知和消息都跳转到 message 页面
|
|
|
+ router.push('/message')
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// WebSocket 事件处理
|
|
|
onMounted(() => {
|
|
|
- console.log('[Navbar] 初始化 WebSocket 连接...')
|
|
|
- // 建立 WebSocket 连接
|
|
|
- wsService.connect()
|
|
|
+ // 获取数据
|
|
|
+ fetchTodoList()
|
|
|
+ fetchNotificationList()
|
|
|
+ fetchMessageList()
|
|
|
+
|
|
|
+ console.log('[Navbar] 初始化 WebSocket 监听器...')
|
|
|
+
|
|
|
+ // 先注册所有监听器,再连接(确保不会错过消息)
|
|
|
|
|
|
// 监听连接状态
|
|
|
wsService.on('connected', () => {
|
|
|
@@ -223,29 +432,69 @@ onMounted(() => {
|
|
|
console.log('[Navbar] WebSocket 已断开')
|
|
|
})
|
|
|
|
|
|
- // 监听未读数更新
|
|
|
- wsService.on('unread_count', (count) => {
|
|
|
- console.log('[Navbar] 未读数更新:', count)
|
|
|
- unreadCount.value = count
|
|
|
- })
|
|
|
-
|
|
|
- // 监听新消息
|
|
|
+ // 监听新消息(私聊)- 弹窗提示并可跳转
|
|
|
wsService.on('new_message', (payload) => {
|
|
|
console.log('[Navbar] 收到新消息:', payload)
|
|
|
- unreadCount.value++
|
|
|
+ // 刷新消息列表
|
|
|
+ fetchMessageList()
|
|
|
+
|
|
|
+ // 弹窗提示
|
|
|
+ const key = `msg_${Date.now()}`
|
|
|
+ notification.info({
|
|
|
+ message: payload.senderName || '新消息',
|
|
|
+ description: payload.content || '您收到一条新消息',
|
|
|
+ key,
|
|
|
+ duration: 5,
|
|
|
+ onClick: () => {
|
|
|
+ notification.close(key)
|
|
|
+ // 跳转到对应会话
|
|
|
+ if (payload.conversationId) {
|
|
|
+ router.push({
|
|
|
+ path: '/message',
|
|
|
+ query: { conversationId: payload.conversationId, t: Date.now() }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ router.push('/message')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
- // 监听新通知
|
|
|
+ // 监听新通知(系统通知)- 弹窗提示
|
|
|
wsService.on('new_notification', (payload) => {
|
|
|
- console.log('[Navbar] 收到新通知:', payload)
|
|
|
- unreadCount.value++
|
|
|
+ console.log('[Navbar] 收到通知:', payload)
|
|
|
+ // 刷新通知列表
|
|
|
+ fetchNotificationList()
|
|
|
+
|
|
|
+ // 弹窗提示
|
|
|
+ const key = `notif_${payload.notification_id || payload.id || Date.now()}`
|
|
|
+ notification.info({
|
|
|
+ message: payload.title || '系统通知',
|
|
|
+ description: truncateText(payload.content, 50) || '您收到一条新通知',
|
|
|
+ key,
|
|
|
+ duration: 5,
|
|
|
+ onClick: () => {
|
|
|
+ notification.close(key)
|
|
|
+ // 显示通知详情弹窗
|
|
|
+ showNotificationDetail({
|
|
|
+ id: payload.notification_id || payload.id,
|
|
|
+ title: payload.title,
|
|
|
+ content: payload.content,
|
|
|
+ time: '刚刚'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
- // 监听系统通知
|
|
|
- wsService.on('system_notification', (payload) => {
|
|
|
- console.log('[Navbar] 收到系统通知:', payload)
|
|
|
- unreadCount.value++
|
|
|
+ // 监听待办更新
|
|
|
+ wsService.on('todo_update', (payload) => {
|
|
|
+ console.log('[Navbar] 待办更新:', payload)
|
|
|
+ fetchTodoList()
|
|
|
})
|
|
|
+
|
|
|
+ // 最后建立 WebSocket 连接
|
|
|
+ console.log('[Navbar] 建立 WebSocket 连接...')
|
|
|
+ wsService.connect()
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
@@ -497,11 +746,31 @@ html.dark .navbar {
|
|
|
padding: 12px 16px;
|
|
|
cursor: pointer;
|
|
|
transition: background 0.2s;
|
|
|
+ position: relative;
|
|
|
|
|
|
&:hover {
|
|
|
background: #f5f5f5;
|
|
|
}
|
|
|
|
|
|
+ &.unread {
|
|
|
+ background: #fafafa;
|
|
|
+
|
|
|
+ .message-title {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1a1a1a;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .unread-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ background: #1890ff;
|
|
|
+ border-radius: 50%;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 8px;
|
|
|
+ margin-top: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
.message-icon {
|
|
|
width: 36px;
|
|
|
height: 36px;
|
|
|
@@ -528,12 +797,33 @@ html.dark .navbar {
|
|
|
color: #fa8c16;
|
|
|
}
|
|
|
|
|
|
+ &.warning {
|
|
|
+ background: #fffbe6;
|
|
|
+ color: #faad14;
|
|
|
+ }
|
|
|
+
|
|
|
&.message {
|
|
|
background: #f9f0ff;
|
|
|
color: #722ed1;
|
|
|
}
|
|
|
|
|
|
- &.todo {
|
|
|
+ // 待办优先级样式
|
|
|
+ &.low {
|
|
|
+ background: #f0f0f0;
|
|
|
+ color: #8c8c8c;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.normal {
|
|
|
+ background: #e6f7ff;
|
|
|
+ color: #1890ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.high {
|
|
|
+ background: #fff7e6;
|
|
|
+ color: #fa8c16;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.urgent {
|
|
|
background: #fff1f0;
|
|
|
color: #f5222d;
|
|
|
}
|
|
|
@@ -593,4 +883,53 @@ html.dark .navbar {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// 通知详情弹窗样式
|
|
|
+.notification-detail-modal {
|
|
|
+ .ant-modal-confirm-content {
|
|
|
+ margin-top: 16px;
|
|
|
+
|
|
|
+ div {
|
|
|
+ line-height: 1.8;
|
|
|
+ color: #333;
|
|
|
+ word-break: break-word;
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ img {
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 通知详情内容样式
|
|
|
+.notification-detail-content {
|
|
|
+ line-height: 1.8;
|
|
|
+ color: #333;
|
|
|
+ word-break: break-word;
|
|
|
+ max-height: 400px;
|
|
|
+ overflow-y: auto;
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ img {
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.notification-detail-time {
|
|
|
+ margin-top: 16px;
|
|
|
+ padding-top: 12px;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
</style>
|