spring
8 天以前 1411afe6c282457c3d7f8a8b069a6c93c9e15b27
完成消息通知前端页面
已添加2个文件
已修改2个文件
468 ■■■■■ 文件已修改
src/api/system/message.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/NotificationCenter/index.vue 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/index.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/message.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
import request from "@/utils/request";
// æŸ¥è¯¢æ¶ˆæ¯é€šçŸ¥åˆ—表
export function listMessage(query) {
  return request({
    url: "/system/message/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢æœªè¯»æ¶ˆæ¯æ•°é‡
export function getUnreadCount() {
  return request({
    url: "/system/message/unreadCount",
    method: "get",
  });
}
// æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
export function markAsRead(messageId) {
  return request({
    url: "/system/message/markAsRead",
    method: "post",
    data: { messageId },
  });
}
// ä¸€é”®æ ‡è®°æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
export function markAllAsRead() {
  return request({
    url: "/system/message/markAllAsRead",
    method: "post",
  });
}
// ç¡®è®¤æ¶ˆæ¯
export function confirmMessage(messageId) {
  return request({
    url: "/system/message/confirm",
    method: "post",
    data: { messageId },
  });
}
src/layout/components/Navbar.vue
@@ -24,6 +24,29 @@
      </el-dropdown>
    </div>
    <div class="right-menu">
      <!-- æ¶ˆæ¯é€šçŸ¥ -->
      <el-popover
        v-model:visible="notificationVisible"
        :width="500"
        placement="bottom-end"
        trigger="click"
        :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }"
        popper-class="notification-popover"
      >
        <template #reference>
          <div class="notification-container right-menu-item hover-effect">
            <el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notification-badge">
              <el-icon :size="20" style="cursor: pointer;">
                <Bell />
              </el-icon>
            </el-badge>
          </div>
        </template>
        <NotificationCenter
          @unreadCountChange="handleUnreadCountChange"
          ref="notificationCenterRef"
        />
      </el-popover>
      <div class="avatar-container">
        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
          <div class="avatar-wrapper">
@@ -51,6 +74,7 @@
<script setup>
import { ElMessageBox } from 'element-plus'
import { Bell } from '@element-plus/icons-vue'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
@@ -59,6 +83,7 @@
import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import NotificationCenter from './NotificationCenter/index.vue'
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
@@ -70,6 +95,9 @@
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const factoryList = ref([])
const notificationVisible = ref(false)
const notificationCenterRef = ref(null)
const unreadCount = ref(0)
function toggleSideBar() {
  appStore.toggleSideBar()
}
@@ -147,6 +175,43 @@
}
getUserLoginFacotryList();
// æ¶ˆæ¯é€šçŸ¥ç›¸å…³
function handleUnreadCountChange(count) {
  unreadCount.value = count
}
// ç»„件挂载时加载未读数量和定时刷新
let unreadCountTimer = null
onMounted(() => {
  // å»¶è¿ŸåŠ è½½ï¼Œç¡®ä¿ç»„ä»¶å·²æ¸²æŸ“
  nextTick(() => {
    if (notificationCenterRef.value) {
      notificationCenterRef.value.loadUnreadCount()
    }
  })
  // å®šæ—¶åˆ·æ–°æœªè¯»æ•°é‡ï¼ˆæ¯30秒)
  unreadCountTimer = setInterval(() => {
    if (notificationCenterRef.value) {
      notificationCenterRef.value.loadUnreadCount()
    }
  }, 30000)
})
// ç›‘听 popover æ˜¾ç¤ºçŠ¶æ€ï¼Œæ‰“å¼€æ—¶åŠ è½½æ¶ˆæ¯åˆ—è¡¨
watch(notificationVisible, (val) => {
  if (val && notificationCenterRef.value) {
    nextTick(() => {
      notificationCenterRef.value.loadMessages()
    })
  }
})
onUnmounted(() => {
  if (unreadCountTimer) {
    clearInterval(unreadCountTimer)
  }
})
</script>
<style lang='scss' scoped>
@@ -241,6 +306,19 @@
      }
    }
    .notification-container {
      margin-right: 20px;
      display: flex;
      align-items: center;
      cursor: pointer;
      .notification-badge {
        :deep(.el-badge__content) {
          border: none;
        }
      }
    }
    .avatar-container {
      margin-right: 40px;
@@ -267,3 +345,17 @@
  }
}
</style>
<style lang="scss">
.notification-popover {
  padding: 0 !important;
  .el-popover__title {
    display: none;
  }
  .el-popover__body {
    padding: 0 !important;
  }
}
</style>
src/layout/components/NotificationCenter/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
<template>
  <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>
    </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-empty description="暂无未读消息" />
          </div>
          <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">
                  <Bell />
                </el-icon>
              </div>
              <div class="notification-content-wrapper">
                <div class="notification-title">{{ item.title }}</div>
                <div class="notification-detail">{{ item.content }}</div>
                <div class="notification-time">{{ item.createTime }}</div>
              </div>
              <div class="notification-action">
                <el-button type="primary" size="small" @click="handleConfirm(item.id)">
                  ç¡®è®¤
                </el-button>
              </div>
            </div>
          </div>
        </el-tab-pane>
        <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 class="notification-icon">
                <el-icon :size="24" color="#909399">
                  <Bell />
                </el-icon>
              </div>
              <div class="notification-content-wrapper">
                <div class="notification-title">{{ item.title }}</div>
                <div class="notification-detail">{{ item.content }}</div>
                <div class="notification-time">{{ item.createTime }}</div>
              </div>
            </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>
    </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'
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 loadMessages = async () => {
  try {
    const params = {
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      isRead: activeTab.value === 'read' ? 1 : 0
    }
    const res = await listMessage(params)
    if (res.code === 200) {
      if (activeTab.value === 'unread') {
        unreadList.value = res.rows || []
      } else {
        readList.value = res.rows || []
      }
      total.value = res.total || 0
    }
  } catch (error) {
    console.error('加载消息列表失败:', error)
  }
}
// åŠ è½½æœªè¯»æ•°é‡
const loadUnreadCount = async () => {
  try {
    const res = await getUnreadCount()
    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 handleConfirm = async (messageId) => {
  try {
    const res = await confirmMessage(messageId)
    if (res.code === 200) {
      ElMessage.success('确认成功')
      // æ ‡è®°ä¸ºå·²è¯»
      await markAsRead(messageId)
      // é‡æ–°åŠ è½½æ•°æ®
      loadMessages()
      loadUnreadCount()
    }
  } catch (error) {
    console.error('确认消息失败:', error)
    ElMessage.error('确认失败')
  }
}
// ä¸€é”®å·²è¯»
const handleMarkAllAsRead = async () => {
  try {
    const res = await markAllAsRead()
    if (res.code === 200) {
      ElMessage.success('已全部标记为已读')
      loadMessages()
      loadUnreadCount()
    }
  } catch (error) {
    console.error('一键已读失败:', error)
    ElMessage.error('操作失败')
  }
}
// åˆ†é¡µå¤§å°æ”¹å˜
const handleSizeChange = (size) => {
  pageSize.value = size
  pageNum.value = 1
  loadMessages()
}
// é¡µç æ”¹å˜
const handlePageChange = (page) => {
  pageNum.value = page
  loadMessages()
}
// ç»„件挂载时加载未读数量
onMounted(() => {
  loadUnreadCount()
})
// ç›‘听父组件传递的 visible çŠ¶æ€ï¼ˆé€šè¿‡ watch åœ¨ Navbar ä¸­å¤„理)
// è¿™é‡Œåªè´Ÿè´£æ•°æ®åŠ è½½ï¼Œä¸æŽ§åˆ¶æ˜¾ç¤º
// æš´éœ²æ–¹æ³•供外部调用
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;
    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>
src/layout/components/index.js
@@ -2,3 +2,4 @@
export { default as Navbar } from './Navbar'
export { default as Settings } from './Settings'
export { default as TagsView } from './TagsView/index.vue'
export { default as NotificationCenter } from './NotificationCenter/index.vue'