| | |
| | | <div class="notification-popover-content"> |
| | | <div class="popover-header"> |
| | | <span class="popover-title">消息通知</span> |
| | | <el-button type="primary" size="small" @click="handleMarkAllAsRead" :disabled="unreadCount === 0"> |
| | | <el-button type="primary" |
| | | size="small" |
| | | @click="handleMarkAllAsRead" |
| | | :disabled="unreadCount === 0"> |
| | | 一键已读 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <div class="notification-content"> |
| | | <el-tabs v-model="activeTab" @tab-change="handleTabChange"> |
| | | <el-tab-pane :label="`未读(${unreadCount})`" name="unread"> |
| | | <div v-if="unreadList.length === 0" class="empty-state"> |
| | | <el-tabs v-model="activeTab" |
| | | @tab-change="handleTabChange"> |
| | | <el-tab-pane :label="`未读(${unreadCount})`" |
| | | name="unread"> |
| | | <div v-if="unreadList.length === 0" |
| | | class="empty-state"> |
| | | <el-empty description="暂无未读消息" /> |
| | | </div> |
| | | <div v-else class="notification-list"> |
| | | <div |
| | | v-for="item in unreadList" |
| | | :key="item.id" |
| | | class="notification-item" |
| | | > |
| | | <div v-else |
| | | class="notification-list"> |
| | | <div v-for="item in unreadList" |
| | | :key="item.id" |
| | | class="notification-item"> |
| | | <div class="notification-icon"> |
| | | <el-icon :size="24" color="#67C23A"> |
| | | <el-icon :size="24" |
| | | color="#67C23A"> |
| | | <Bell /> |
| | | </el-icon> |
| | | </div> |
| | |
| | | <div class="notification-time">{{ item.createTime }}</div> |
| | | </div> |
| | | <div class="notification-action"> |
| | | <el-button type="primary" size="small" @click="handleConfirm(item)"> |
| | | <el-button type="primary" |
| | | size="small" |
| | | @click="handleConfirm(item)"> |
| | | 确认 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="已读" name="read"> |
| | | <div v-if="readList.length === 0" class="empty-state"> |
| | | <el-tab-pane label="已读" |
| | | name="read"> |
| | | <div v-if="readList.length === 0" |
| | | class="empty-state"> |
| | | <el-empty description="暂无已读消息" /> |
| | | </div> |
| | | <div v-else class="notification-list"> |
| | | <div |
| | | v-for="item in readList" |
| | | :key="item.id" |
| | | class="notification-item read" |
| | | > |
| | | <div v-else |
| | | class="notification-list"> |
| | | <div v-for="item in readList" |
| | | :key="item.id" |
| | | class="notification-item read"> |
| | | <div class="notification-icon"> |
| | | <el-icon :size="24" color="#909399"> |
| | | <el-icon :size="24" |
| | | color="#909399"> |
| | | <Bell /> |
| | | </el-icon> |
| | | </div> |
| | |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <!-- 分页 --> |
| | | <div class="pagination-wrapper" v-if="total > 0"> |
| | | <el-pagination |
| | | v-model:current-page="pageNum" |
| | | v-model:page-size="pageSize" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="total" |
| | | layout="prev, pager, next, sizes" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handlePageChange" |
| | | /> |
| | | <div class="pagination-wrapper" |
| | | v-if="total > 0"> |
| | | <el-pagination v-model:current-page="pageNum" |
| | | v-model:page-size="pageSize" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="total" |
| | | layout="prev, pager, next, sizes" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handlePageChange" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Bell } from '@element-plus/icons-vue' |
| | | import { listMessage, markAsRead, markAllAsRead, confirmMessage, getUnreadCount } from '@/api/system/message' |
| | | import { ElMessage } from 'element-plus' |
| | | import useUserStore from '@/store/modules/user' |
| | | import { useRouter } from 'vue-router' |
| | | import { Bell } from "@element-plus/icons-vue"; |
| | | import { |
| | | listMessage, |
| | | markAsRead, |
| | | markAllAsRead, |
| | | confirmMessage, |
| | | getUnreadCount, |
| | | } from "@/api/system/message"; |
| | | import { ElMessage } from "element-plus"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { useRouter } from "vue-router"; |
| | | |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | const emit = defineEmits(['unreadCountChange']) |
| | | const userStore = useUserStore(); |
| | | const router = useRouter(); |
| | | const emit = defineEmits(["unreadCountChange"]); |
| | | |
| | | const activeTab = ref('unread') |
| | | const unreadList = ref([]) |
| | | const readList = ref([]) |
| | | const unreadCount = ref(0) |
| | | const total = ref(0) |
| | | const pageNum = ref(1) |
| | | const pageSize = ref(10) |
| | | const activeTab = ref("unread"); |
| | | const unreadList = ref([]); |
| | | const readList = ref([]); |
| | | const unreadCount = ref(0); |
| | | const total = ref(0); |
| | | const pageNum = ref(1); |
| | | const pageSize = ref(10); |
| | | |
| | | // 加载消息列表 |
| | | const loadMessages = async () => { |
| | | try { |
| | | const consigneeId = userStore.id |
| | | if (!consigneeId) { |
| | | console.warn('未获取到当前登录用户ID') |
| | | return |
| | | } |
| | | const params = { |
| | | consigneeId: consigneeId, |
| | | current: pageNum.value, |
| | | size: pageSize.value, |
| | | status: activeTab.value === 'read' ? 1 : 0 |
| | | } |
| | | const res = await listMessage(params) |
| | | if (res.code === 200) { |
| | | if (activeTab.value === 'unread') { |
| | | unreadList.value = res.data.records || [] |
| | | } else { |
| | | readList.value = res.data.records || [] |
| | | // 加载消息列表 |
| | | const loadMessages = async () => { |
| | | try { |
| | | const consigneeId = userStore.id; |
| | | if (!consigneeId) { |
| | | console.warn("未获取到当前登录用户ID"); |
| | | return; |
| | | } |
| | | total.value = res.data.total || 0 |
| | | const params = { |
| | | consigneeId: consigneeId, |
| | | current: pageNum.value, |
| | | size: pageSize.value, |
| | | status: activeTab.value === "read" ? 1 : 0, |
| | | }; |
| | | const res = await listMessage(params); |
| | | if (res.code === 200) { |
| | | if (activeTab.value === "unread") { |
| | | unreadList.value = res.data.records || []; |
| | | } else { |
| | | readList.value = res.data.records || []; |
| | | } |
| | | total.value = res.data.total || 0; |
| | | } |
| | | } catch (error) { |
| | | console.error("加载消息列表失败:", error); |
| | | } |
| | | } catch (error) { |
| | | console.error('加载消息列表失败:', error) |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 加载未读数量 |
| | | const loadUnreadCount = async () => { |
| | | try { |
| | | const consigneeId = userStore.id |
| | | if (!consigneeId) { |
| | | console.warn('未获取到当前登录用户ID') |
| | | return |
| | | // 加载未读数量 |
| | | const loadUnreadCount = async () => { |
| | | try { |
| | | const consigneeId = userStore.id; |
| | | if (!consigneeId) { |
| | | console.warn("未获取到当前登录用户ID"); |
| | | return; |
| | | } |
| | | const res = await getUnreadCount(consigneeId); |
| | | if (res.code === 200) { |
| | | unreadCount.value = res.data || 0; |
| | | emit("unreadCountChange", unreadCount.value); |
| | | } |
| | | } catch (error) { |
| | | console.error("加载未读数量失败:", error); |
| | | } |
| | | const res = await getUnreadCount(consigneeId) |
| | | if (res.code === 200) { |
| | | unreadCount.value = res.data || 0 |
| | | emit('unreadCountChange', unreadCount.value) |
| | | } |
| | | } catch (error) { |
| | | console.error('加载未读数量失败:', error) |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 标签页切换 |
| | | const handleTabChange = (tab) => { |
| | | pageNum.value = 1 |
| | | loadMessages() |
| | | } |
| | | // 标签页切换 |
| | | const handleTabChange = tab => { |
| | | pageNum.value = 1; |
| | | loadMessages(); |
| | | }; |
| | | |
| | | // 确认消息 |
| | | const handleConfirm = async (item) => { |
| | | try { |
| | | console.log('item', item) |
| | | const res = await confirmMessage(item.noticeId, 1) |
| | | if (res.code === 200) { |
| | | ElMessage.success('确认成功') |
| | | // 重新加载数据 |
| | | loadMessages() |
| | | loadUnreadCount() |
| | | |
| | | // 根据 jumpPath 进行页面跳转 |
| | | if (item.jumpPath) { |
| | | try { |
| | | // 解析 jumpPath,分离路径和查询参数 |
| | | const [path, queryString] = item.jumpPath.split('?') |
| | | let query = {} |
| | | |
| | | if (queryString) { |
| | | // 解析查询参数 |
| | | queryString.split('&').forEach(param => { |
| | | const [key, value] = param.split('=') |
| | | if (key && value) { |
| | | query[key] = decodeURIComponent(value) |
| | | } |
| | | }) |
| | | // 确认消息 |
| | | const handleConfirm = async item => { |
| | | try { |
| | | console.log("item", item); |
| | | const res = await confirmMessage(item.noticeId, 1); |
| | | if (res.code === 200) { |
| | | ElMessage.success("确认成功"); |
| | | // 重新加载数据 |
| | | loadMessages(); |
| | | loadUnreadCount(); |
| | | |
| | | // 根据 jumpPath 进行页面跳转 |
| | | if (item.jumpPath) { |
| | | try { |
| | | // 解析 jumpPath,分离路径和查询参数 |
| | | const [path, queryString] = item.jumpPath.split("?"); |
| | | let query = {}; |
| | | |
| | | if (queryString) { |
| | | // 解析查询参数 |
| | | queryString.split("&").forEach(param => { |
| | | const [key, value] = param.split("="); |
| | | if (key && value) { |
| | | query[key] = decodeURIComponent(value); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // 跳转到指定页面 |
| | | router.push({ |
| | | path: path, |
| | | query: query, |
| | | }); |
| | | } catch (error) { |
| | | console.error("页面跳转失败:", error); |
| | | } |
| | | |
| | | // 跳转到指定页面 |
| | | router.push({ |
| | | path: path, |
| | | query: query |
| | | }) |
| | | } catch (error) { |
| | | console.error('页面跳转失败:', error) |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error("确认消息失败:", error); |
| | | ElMessage.error("确认失败"); |
| | | } |
| | | } catch (error) { |
| | | console.error('确认消息失败:', error) |
| | | ElMessage.error('确认失败') |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 一键已读 |
| | | const handleMarkAllAsRead = async () => { |
| | | try { |
| | | const res = await markAllAsRead() |
| | | if (res.code === 200) { |
| | | ElMessage.success('已全部标记为已读') |
| | | loadMessages() |
| | | loadUnreadCount() |
| | | // 一键已读 |
| | | const handleMarkAllAsRead = async () => { |
| | | try { |
| | | const res = await markAllAsRead(); |
| | | if (res.code === 200) { |
| | | ElMessage.success("已全部标记为已读"); |
| | | loadMessages(); |
| | | loadUnreadCount(); |
| | | } |
| | | } catch (error) { |
| | | console.error("一键已读失败:", error); |
| | | ElMessage.error("操作失败"); |
| | | } |
| | | } catch (error) { |
| | | console.error('一键已读失败:', error) |
| | | ElMessage.error('操作失败') |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 分页大小改变 |
| | | const handleSizeChange = (size) => { |
| | | pageSize.value = size |
| | | pageNum.value = 1 |
| | | loadMessages() |
| | | } |
| | | // 分页大小改变 |
| | | const handleSizeChange = size => { |
| | | pageSize.value = size; |
| | | pageNum.value = 1; |
| | | loadMessages(); |
| | | }; |
| | | |
| | | // 页码改变 |
| | | const handlePageChange = (page) => { |
| | | pageNum.value = page |
| | | loadMessages() |
| | | } |
| | | // 页码改变 |
| | | const handlePageChange = page => { |
| | | pageNum.value = page; |
| | | loadMessages(); |
| | | }; |
| | | |
| | | // 组件挂载时加载未读数量 |
| | | onMounted(() => { |
| | | loadUnreadCount() |
| | | }) |
| | | // 组件挂载时加载未读数量 |
| | | onMounted(() => { |
| | | loadUnreadCount(); |
| | | }); |
| | | |
| | | // 监听父组件传递的 visible 状态(通过 watch 在 Navbar 中处理) |
| | | // 这里只负责数据加载,不控制显示 |
| | | // 监听父组件传递的 visible 状态(通过 watch 在 Navbar 中处理) |
| | | // 这里只负责数据加载,不控制显示 |
| | | |
| | | // 暴露方法供外部调用 |
| | | defineExpose({ |
| | | loadUnreadCount, |
| | | loadMessages |
| | | }) |
| | | // 暴露方法供外部调用 |
| | | defineExpose({ |
| | | loadUnreadCount, |
| | | loadMessages, |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .notification-popover-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 500px; |
| | | padding: 16px; |
| | | } |
| | | |
| | | .popover-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | width: 100%; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | |
| | | .popover-title { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | } |
| | | |
| | | .notification-content { |
| | | max-height: 60vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.el-tabs) { |
| | | flex: 1; |
| | | .notification-popover-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 0; |
| | | |
| | | .el-tabs__header { |
| | | margin-bottom: 0; |
| | | flex-shrink: 0; |
| | | padding: 0; |
| | | } |
| | | |
| | | .el-tabs__content { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | min-height: 0; |
| | | padding-top: 16px; |
| | | } |
| | | |
| | | .el-tab-pane { |
| | | height: 100%; |
| | | } |
| | | width: 500px; |
| | | padding: 16px; |
| | | } |
| | | } |
| | | |
| | | .empty-state { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | min-height: 300px; |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .notification-list { |
| | | .notification-item { |
| | | .popover-header { |
| | | display: flex; |
| | | padding: 12px 0; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | width: 100%; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | transition: background-color 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | &.read { |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | .notification-icon { |
| | | flex-shrink: 0; |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background-color: #f0f9ff; |
| | | border-radius: 50%; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | | .notification-content-wrapper { |
| | | flex: 1; |
| | | min-width: 0; |
| | | |
| | | .notification-title { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .notification-detail { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | line-height: 1.5; |
| | | margin-bottom: 8px; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .notification-time { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .notification-action { |
| | | flex-shrink: 0; |
| | | margin-left: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | .popover-title { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .pagination-wrapper { |
| | | margin-top: 16px; |
| | | padding-top: 16px; |
| | | border-top: 1px solid #f0f0f0; |
| | | display: flex; |
| | | justify-content: center; |
| | | padding-left: 0; |
| | | padding-right: 0; |
| | | } |
| | | .notification-content { |
| | | max-height: 60vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.el-tabs) { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 0; |
| | | |
| | | .el-tabs__header { |
| | | margin-bottom: 0; |
| | | flex-shrink: 0; |
| | | padding: 0; |
| | | } |
| | | |
| | | .el-tabs__content { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | min-height: 0; |
| | | padding-top: 16px; |
| | | } |
| | | |
| | | .el-tab-pane { |
| | | height: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .empty-state { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | min-height: 300px; |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .notification-list { |
| | | .notification-item { |
| | | display: flex; |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | transition: background-color 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | &.read { |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | .notification-icon { |
| | | flex-shrink: 0; |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background-color: #f0f9ff; |
| | | border-radius: 50%; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | | .notification-content-wrapper { |
| | | flex: 1; |
| | | min-width: 0; |
| | | |
| | | .notification-title { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .notification-detail { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | line-height: 1.5; |
| | | margin-bottom: 8px; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .notification-time { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .notification-action { |
| | | flex-shrink: 0; |
| | | margin-left: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .pagination-wrapper { |
| | | margin-top: 16px; |
| | | padding-top: 16px; |
| | | border-top: 1px solid #f0f0f0; |
| | | display: flex; |
| | | justify-content: center; |
| | | padding-left: 0; |
| | | padding-right: 0; |
| | | } |
| | | </style> |
| | | |