瀏覽代碼

消息弹窗配置

wangxy 7 小時之前
父節點
當前提交
33b9eb197d

+ 29 - 0
src/api/crm/complaintMsg.js

@@ -0,0 +1,29 @@
+import request from '@/utils/request'
+
+export function getLatestUnreadMsg() {
+  return request({
+    url: '/crm/complaintMsg/getLatestUnread',
+    method: 'get'
+  })
+}
+
+export function getUnreadCount() {
+  return request({
+    url: '/crm/complaintMsg/getUnreadCount',
+    method: 'get'
+  })
+}
+
+export function markAsRead(msgId) {
+  return request({
+    url: `/crm/complaintMsg/markAsRead/${msgId}`,
+    method: 'post'
+  })
+}
+
+export function markAllAsRead() {
+  return request({
+    url: '/crm/complaintMsg/markAllAsRead',
+    method: 'post'
+  })
+}

+ 29 - 0
src/api/crm/integralOrderMsg.js

@@ -0,0 +1,29 @@
+import request from '@/utils/request'
+
+export function getLatestUnreadMsg() {
+  return request({
+    url: '/crm/integralOrderMsg/getLatestUnread',
+    method: 'get'
+  })
+}
+
+export function getUnreadCount() {
+  return request({
+    url: '/crm/integralOrderMsg/getUnreadCount',
+    method: 'get'
+  })
+}
+
+export function markAsRead(msgId) {
+  return request({
+    url: `/crm/integralOrderMsg/markAsRead/${msgId}`,
+    method: 'post'
+  })
+}
+
+export function markAllAsRead() {
+  return request({
+    url: '/crm/integralOrderMsg/markAllAsRead',
+    method: 'post'
+  })
+}

+ 39 - 8
src/api/crm/msg.js

@@ -1,6 +1,42 @@
 import request from '@/utils/request'
 
-// 查询 消息列表
+export function getMsg() {
+  return request({
+    url: '/crm/msg/getMsg',
+    method: 'get'
+  })
+}
+
+export function getMsgList(query) {
+  return request({
+    url: '/crm/msg/getMsgList',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getMsgCount() {
+  return request({
+    url: '/crm/msg/getMsgCount',
+    method: 'get',
+  })
+}
+
+export function setRead(data) {
+  return request({
+    url: '/crm/msg/setRead',
+    method: 'post',
+    data: data
+  })
+}
+
+export function setAllRead() {
+  return request({
+    url: '/crm/msg/setAllRead',
+    method: 'post'
+  })
+}
+
 export function listMsg(query) {
   return request({
     url: '/crm/msg/list',
@@ -9,15 +45,13 @@ export function listMsg(query) {
   })
 }
 
-// 查询 消息详细
-export function getMsg(msgId) {
+export function getMsgDetail(msgId) {
   return request({
     url: '/crm/msg/' + msgId,
     method: 'get'
   })
 }
 
-// 新增 消息
 export function addMsg(data) {
   return request({
     url: '/crm/msg',
@@ -26,7 +60,6 @@ export function addMsg(data) {
   })
 }
 
-// 修改 消息
 export function updateMsg(data) {
   return request({
     url: '/crm/msg',
@@ -35,7 +68,6 @@ export function updateMsg(data) {
   })
 }
 
-// 删除 消息
 export function delMsg(msgId) {
   return request({
     url: '/crm/msg/' + msgId,
@@ -43,11 +75,10 @@ export function delMsg(msgId) {
   })
 }
 
-// 导出 消息
 export function exportMsg(query) {
   return request({
     url: '/crm/msg/export',
     method: 'get',
     params: query
   })
-}
+}

+ 262 - 0
src/components/ComplaintMsgDialog/index.vue

@@ -0,0 +1,262 @@
+<template>
+  <el-dialog
+    title="投诉提醒"
+    :visible.sync="visible"
+    width="480px"
+    :close-on-click-modal="false"
+    :show-close="true"
+    custom-class="complaint-msg-dialog"
+    @close="handleClose"
+    center
+  >
+    <div class="msg-content">
+      <div class="msg-icon">
+        <i class="el-icon-warning-outline"></i>
+      </div>
+      <div class="msg-text">
+        <h3>{{ msgData.title }}</h3>
+        <p>{{ msgData.content }}</p>
+        <div class="msg-stats">
+          <div class="stat-item">
+            <span class="stat-label">未处理消息</span>
+            <span class="stat-value">{{ unreadCount }}条</span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">未处理率</span>
+            <span class="stat-value percent">{{ unreadPercent }}%</span>
+          </div>
+        </div>
+        <div class="msg-time" v-if="msgData.createTime">
+          <i class="el-icon-time"></i>
+          {{ msgData.createTime }}
+        </div>
+      </div>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="handleKnow" size="medium">
+        <i class="el-icon-check"></i> 知道了
+      </el-button>
+      <el-button type="primary" @click="handleGoProcess" size="medium">
+        <i class="el-icon-right"></i> 去处理
+      </el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import { getLatestUnreadMsg, markAsRead } from '@/api/crm/complaintMsg'
+
+export default {
+  name: 'ComplaintMsgDialog',
+  data() {
+    return {
+      visible: false,
+      msgData: {
+        msgId: null,
+        title: '',
+        content: '',
+        actionUrl: '',
+        createTime: ''
+      },
+      unreadCount: 0,
+      totalCount: 0
+    }
+  },
+  computed: {
+    unreadPercent() {
+      if (this.totalCount === 0) return 0
+      return Math.round((this.unreadCount / this.totalCount) * 100)
+    }
+  },
+  methods: {
+    checkNewMsg() {
+      getLatestUnreadMsg().then(res => {
+        if (res.code === 200 && res.data) {
+          if (res.data.msg) {
+            this.msgData = res.data.msg
+            this.unreadCount = res.data.unreadCount || 0
+            this.totalCount = res.data.totalCount || 0
+            this.visible = true
+          }
+        }
+      }).catch(err => {
+        console.error('获取投诉消息失败:', err)
+      })
+    },
+    handleKnow() {
+      this.visible = false
+    },
+    handleGoProcess() {
+      if (this.msgData.msgId) {
+        markAsRead(this.msgData.msgId).then(() => {
+          this.visible = false
+          this.$emit('msg-read')
+          this.navigateToUrl('/course/userCourseComplaintRecord')
+          setTimeout(() => {
+            this.checkNewMsg()
+          }, 500)
+        })
+      } else {
+        this.visible = false
+        this.navigateToUrl('/course/userCourseComplaintRecord')
+      }
+    },
+    navigateToUrl(url) {
+      if (url.endsWith('/index')) {
+        url = url.slice(0, -6)
+      }
+      if (!url.startsWith('/')) {
+        url = '/' + url
+      }
+      this.$router.push(url).catch(err => {
+        console.error('路由跳转失败:', err)
+      })
+    },
+    handleClose() {
+      this.visible = false
+    },
+    show() {
+      this.checkNewMsg()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.complaint-msg-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.msg-content {
+  display: flex;
+  align-items: flex-start;
+  padding: 24px 16px;
+}
+
+.msg-icon {
+  flex-shrink: 0;
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20px;
+  box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
+}
+
+.msg-icon i {
+  font-size: 28px;
+  color: #fff;
+  animation: shake 0.5s ease-in-out infinite;
+}
+
+@keyframes shake {
+  0%, 100% {
+    transform: translateX(0);
+  }
+  25% {
+    transform: translateX(-3px);
+  }
+  75% {
+    transform: translateX(3px);
+  }
+}
+
+.msg-text {
+  flex: 1;
+}
+
+.msg-text h3 {
+  margin: 0 0 12px 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.msg-text p {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.msg-stats {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 12px;
+  padding: 12px;
+  background: #fef0f0;
+  border-radius: 8px;
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.stat-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  font-size: 20px;
+  font-weight: 600;
+  color: #f56c6c;
+}
+
+.stat-value.percent {
+  color: #e6a23c;
+}
+
+.msg-time {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+}
+
+.msg-time i {
+  margin-right: 4px;
+}
+
+.dialog-footer {
+  text-align: center;
+  padding: 10px 20px 20px;
+}
+
+.dialog-footer .el-button {
+  min-width: 100px;
+}
+
+.dialog-footer .el-button i {
+  margin-right: 4px;
+}
+</style>
+
+<style>
+.complaint-msg-dialog .el-dialog__header {
+  background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
+  padding: 16px 20px;
+}
+
+.complaint-msg-dialog .el-dialog__title {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.complaint-msg-dialog .el-dialog__headerbtn .el-dialog__close {
+  color: #fff;
+  font-size: 18px;
+}
+
+.complaint-msg-dialog .el-dialog__body {
+  padding: 0;
+}
+</style>

+ 269 - 0
src/components/IntegralOrderMsgDialog/index.vue

@@ -0,0 +1,269 @@
+<template>
+  <el-dialog
+    title="订单提醒"
+    :visible.sync="visible"
+    width="480px"
+    :close-on-click-modal="false"
+    :show-close="true"
+    custom-class="integral-order-msg-dialog"
+    @close="handleClose"
+    center
+  >
+    <div class="msg-content">
+      <div class="msg-icon">
+        <i class="el-icon-bell"></i>
+      </div>
+      <div class="msg-text">
+        <h3>{{ msgData.title }}</h3>
+        <p>{{ msgData.content }}</p>
+        <div class="msg-stats">
+          <div class="stat-item">
+            <span class="stat-label">未处理消息</span>
+            <span class="stat-value">{{ unreadCount }}条</span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">未处理率</span>
+            <span class="stat-value percent">{{ unreadPercent }}%</span>
+          </div>
+        </div>
+        <div class="msg-time" v-if="msgData.createTime">
+          <i class="el-icon-time"></i>
+          {{ msgData.createTime }}
+        </div>
+      </div>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="handleKnow" size="medium">
+        <i class="el-icon-check"></i> 知道了
+      </el-button>
+      <el-button type="primary" @click="handleGoProcess" size="medium">
+        <i class="el-icon-right"></i> 去处理
+      </el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import { getLatestUnreadMsg, markAsRead } from '@/api/crm/integralOrderMsg'
+
+export default {
+  name: 'IntegralOrderMsgDialog',
+  data() {
+    return {
+      visible: false,
+      msgData: {
+        msgId: null,
+        title: '',
+        content: '',
+        actionUrl: '',
+        createTime: ''
+      },
+      unreadCount: 0,
+      totalCount: 0
+    }
+  },
+  computed: {
+    unreadPercent() {
+      if (this.totalCount === 0) return 0
+      return Math.round((this.unreadCount / this.totalCount) * 100)
+    }
+  },
+  methods: {
+    checkNewMsg() {
+      getLatestUnreadMsg().then(res => {
+        if (res.code === 200 && res.data) {
+          if (res.data.msg) {
+            this.msgData = res.data.msg
+            this.unreadCount = res.data.unreadCount || 0
+            this.totalCount = res.data.totalCount || 0
+            this.visible = true
+          }
+        }
+      }).catch(err => {
+        console.error('获取消息失败:', err)
+      })
+    },
+    handleKnow() {
+      this.visible = false
+    },
+    handleGoProcess() {
+      if (this.msgData.msgId) {
+        markAsRead(this.msgData.msgId).then(() => {
+          this.visible = false
+          this.$emit('msg-read')
+          if (this.msgData.actionUrl) {
+            this.navigateToUrl(this.msgData.actionUrl)
+          }
+          setTimeout(() => {
+            this.checkNewMsg()
+          }, 500)
+        })
+      } else {
+        this.visible = false
+        if (this.msgData.actionUrl) {
+          this.navigateToUrl(this.msgData.actionUrl)
+        }
+      }
+    },
+    navigateToUrl(url) {
+      if (url.endsWith('/index')) {
+        url = url.slice(0, -6)
+      }
+      if (!url.startsWith('/')) {
+        url = '/' + url
+      }
+      this.$router.push(url).catch(err => {
+        console.error('路由跳转失败:', err)
+      })
+    },
+    handleClose() {
+      this.visible = false
+    },
+    show() {
+      this.checkNewMsg()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.integral-order-msg-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.msg-content {
+  display: flex;
+  align-items: flex-start;
+  padding: 24px 16px;
+}
+
+.msg-icon {
+  flex-shrink: 0;
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20px;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.msg-icon i {
+  font-size: 28px;
+  color: #fff;
+  animation: bell-ring 1s ease-in-out infinite;
+}
+
+@keyframes bell-ring {
+  0%, 100% {
+    transform: rotate(0);
+  }
+  10%, 30% {
+    transform: rotate(10deg);
+  }
+  20%, 40% {
+    transform: rotate(-10deg);
+  }
+  50% {
+    transform: rotate(0);
+  }
+}
+
+.msg-text {
+  flex: 1;
+}
+
+.msg-text h3 {
+  margin: 0 0 12px 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.msg-text p {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.msg-stats {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 12px;
+  padding: 12px;
+  background: #f0f9eb;
+  border-radius: 8px;
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.stat-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  font-size: 20px;
+  font-weight: 600;
+  color: #67c23a;
+}
+
+.stat-value.percent {
+  color: #e6a23c;
+}
+
+.msg-time {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+}
+
+.msg-time i {
+  margin-right: 4px;
+}
+
+.dialog-footer {
+  text-align: center;
+  padding: 10px 20px 20px;
+}
+
+.dialog-footer .el-button {
+  min-width: 100px;
+}
+
+.dialog-footer .el-button i {
+  margin-right: 4px;
+}
+</style>
+
+<style>
+.integral-order-msg-dialog .el-dialog__header {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 16px 20px;
+}
+
+.integral-order-msg-dialog .el-dialog__title {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.integral-order-msg-dialog .el-dialog__headerbtn .el-dialog__close {
+  color: #fff;
+  font-size: 18px;
+}
+
+.integral-order-msg-dialog .el-dialog__body {
+  padding: 0;
+}
+</style>

+ 100 - 1
src/layout/components/Navbar.vue

@@ -7,6 +7,14 @@
 
     <div class="right-menu">
       <template v-if="device!=='mobile'">
+        <div class="right-menu-item hover-effect">
+          <el-badge v-if="msgCount>0" :value="msgCount" :max="99" class="dot">
+          </el-badge>
+          <div class="msg" @click="openMsg()">
+            <i class="el-icon-message-solid"></i>
+          </div>
+        </div>
+
         <search id="header-search" class="right-menu-item" />
 
         <screenfull id="screenfull" class="right-menu-item hover-effect" />
@@ -35,6 +43,18 @@
         </el-dropdown-menu>
       </el-dropdown>
     </div>
+
+    <el-drawer
+      :append-to-body="true"
+      size="35%"
+      :with-header="false"
+      :visible.sync="msg.open"
+    >
+      <msg ref="msg" @update-count="getMsgCount" />
+    </el-drawer>
+
+    <integral-order-msg-dialog ref="integralOrderMsgDialog" @msg-read="getMsgCount" />
+    <complaint-msg-dialog ref="complaintMsgDialog" @msg-read="getMsgCount" />
   </div>
 </template>
 
@@ -46,7 +66,10 @@ import Hamburger from '@/components/Hamburger'
 import Screenfull from '@/components/Screenfull'
 import SizeSelect from '@/components/SizeSelect'
 import Search from '@/components/HeaderSearch'
-
+import msg from "@/views/crm/components/msg";
+import IntegralOrderMsgDialog from '@/components/IntegralOrderMsgDialog'
+import ComplaintMsgDialog from '@/components/ComplaintMsgDialog'
+import { getMsg, getMsgList, getMsgCount, setRead } from "@/api/crm/msg";
 
 export default {
   components: {
@@ -56,6 +79,9 @@ export default {
     Screenfull,
     SizeSelect,
     Search,
+    msg,
+    IntegralOrderMsgDialog,
+    ComplaintMsgDialog
   },
   computed: {
     ...mapGetters([
@@ -80,7 +106,68 @@ export default {
       }
     }
   },
+  data() {
+    return {
+      msgCount: 0,
+      previousMsgCount: 0,
+      msg: {
+        open: false,
+        title: '通知消息'
+      },
+      msgPollingTimer: null,
+      isFirstCheck: true,
+    }
+  },
+  created() {
+    this.getMsgCount();
+    this.startMsgPolling();
+  },
+  beforeDestroy() {
+    this.stopMsgPolling();
+  },
   methods: {
+    startMsgPolling() {
+      if (this.msgPollingTimer) {
+        clearInterval(this.msgPollingTimer);
+      }
+      this.msgPollingTimer = setInterval(() => {
+        this.getMsgCount();
+      }, 30000);
+    },
+    stopMsgPolling() {
+      if (this.msgPollingTimer) {
+        clearInterval(this.msgPollingTimer);
+        this.msgPollingTimer = null;
+      }
+    },
+    checkSpecialMsgs() {
+      if (this.$refs.integralOrderMsgDialog) {
+        this.$refs.integralOrderMsgDialog.show();
+      }
+      if (this.$refs.complaintMsgDialog) {
+        this.$refs.complaintMsgDialog.show();
+      }
+    },
+    getMsgCount() {
+      getMsg().then(response => {
+        let totalCount = 0;
+        response.counts.forEach(item => {
+          totalCount += item.total;
+        });
+        this.msgCount = totalCount;
+
+        if (this.isFirstCheck) {
+          this.isFirstCheck = false;
+          this.$nextTick(() => {
+            this.checkSpecialMsgs();
+          });
+        }
+        this.previousMsgCount = totalCount;
+      });
+    },
+    openMsg() {
+      this.msg.open = true;
+    },
     toggleSideBar() {
       this.$store.dispatch('app/toggleSideBar')
     },
@@ -161,6 +248,18 @@ export default {
       }
     }
 
+    .msg {
+      font-size: 20px;
+      cursor: pointer;
+      position: relative;
+    }
+
+    .dot {
+      position: absolute;
+      top: 5px;
+      right: -5px;
+    }
+
     .avatar-container {
       margin-right: 30px;
 

+ 305 - 0
src/views/crm/components/msg.vue

@@ -0,0 +1,305 @@
+<template>
+  <div class="msg">
+    <div class="msg-title">
+      通知消息
+      <el-button round size="small" @click="setAllRead()">全部已读</el-button>
+    </div>
+
+    <div class="msg-type">
+      <div class="msg-type-item" v-for="(item, index) in msgType" :key="index" @click="getMsgList(item.msgType)">
+        <el-badge :value="item.total" :max="99" :hidden="item.total==0" class="item">
+          <el-button round size="small">{{item.msgTypeName}}</el-button>
+        </el-badge>
+      </div>
+    </div>
+
+    <div class="mst-list">
+      <el-timeline>
+        <el-timeline-item :timestamp="item.createTime" placement="top" v-for="(item, index) in list" :key="index">
+          <el-card>
+            <h4>{{item.title}}</h4>
+            <p>{{item.content}}</p>
+            <div>
+              <el-button type="text" v-if="item.isRead==0" @click="setRead(item)">标记为已读</el-button>
+              <el-button type="text" @click="showDetail(item)">查看详情</el-button>
+            </div>
+          </el-card>
+        </el-timeline-item>
+      </el-timeline>
+      <div v-if="isMore" class="more" @click="loadMore()">
+        <el-button type="text">加载更多</el-button>
+      </div>
+    </div>
+
+    <el-dialog
+      title="消息详情"
+      :visible.sync="detailVisible"
+      width="480px"
+      :close-on-click-modal="false"
+      :append-to-body="true"
+      :modal-append-to-body="true"
+      custom-class="msg-detail-dialog"
+      center
+    >
+      <div class="msg-content">
+        <div class="msg-icon">
+          <i class="el-icon-bell"></i>
+        </div>
+        <div class="msg-text">
+          <h3>{{ detailData.title }}</h3>
+          <p>{{ detailData.content }}</p>
+          <div class="msg-time" v-if="detailData.createTime">
+            <i class="el-icon-time"></i>
+            {{ detailData.createTime }}
+          </div>
+        </div>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="closeDetail" size="medium">关闭</el-button>
+        <el-button type="primary" @click="goToProcess" size="medium" v-if="detailData.actionUrl">
+          <i class="el-icon-right"></i> 去处理
+        </el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { setAllRead, getMsg, getMsgList, getMsgCount, setRead } from "@/api/crm/msg";
+
+export default {
+  name: "msg",
+  data() {
+    return {
+      isMore: false,
+      currentTypeId: undefined,
+      msgType: [],
+      loading: true,
+      ids: [],
+      single: true,
+      multiple: true,
+      showSearch: true,
+      total: 0,
+      list: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+      },
+      detailVisible: false,
+      detailData: {
+        msgId: null,
+        title: '',
+        content: '',
+        actionUrl: '',
+        createTime: ''
+      }
+    };
+  },
+  created() {
+    this.getMsg();
+  },
+  methods: {
+    loadMore() {
+      this.queryParams.pageNum += 1;
+      this.getList();
+    },
+    getMsg() {
+      getMsg().then(response => {
+        this.msgType = response.counts;
+        if (this.currentTypeId == undefined) {
+          this.currentTypeId = this.msgType[0].msgType;
+        }
+        this.list = [];
+        this.getList();
+      });
+    },
+    setAllRead() {
+      setAllRead().then(response => {
+        this.getMsg();
+        this.$emit('update-count');
+      });
+    },
+    setRead(row) {
+      row.isRead = 1;
+      setRead(row).then(response => {
+        this.getMsg();
+        this.$emit('update-count');
+      });
+    },
+    getMsgList(typeId) {
+      this.currentTypeId = typeId;
+      this.queryParams.pageNum = 1;
+      this.list = [];
+      this.getList();
+    },
+    getList() {
+      this.loading = true;
+      this.queryParams.type = this.currentTypeId;
+      getMsgList(this.queryParams).then(response => {
+        this.list = this.list.concat(response.data.list);
+        this.total = response.data.total;
+        this.loading = false;
+        this.isMore = response.data.hasNextPage;
+      });
+    },
+    showDetail(item) {
+      this.detailData = item;
+      this.detailVisible = true;
+    },
+    closeDetail() {
+      this.detailVisible = false;
+    },
+    goToProcess() {
+      if (this.detailData.msgId && this.detailData.isRead == 0) {
+        this.detailData.isRead = 1;
+        setRead(this.detailData).then(() => {
+          this.getMsg();
+          this.$emit('update-count');
+        });
+      }
+      this.detailVisible = false;
+      if (this.detailData.actionUrl) {
+        let url = this.detailData.actionUrl;
+        if (url.endsWith('/index')) {
+          url = url.slice(0, -6)
+        }
+        if (!url.startsWith('/')) {
+          url = '/' + url
+        }
+        this.$router.push({ path: url }).catch(err => {
+          console.error('路由跳转失败:', err)
+        });
+      }
+    },
+  }
+};
+</script>
+
+<style scoped>
+.msg {
+  padding: 15px;
+}
+
+.dialog-footer {
+  margin-top: 30px;
+  float: right;
+}
+
+.msg-type {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+
+.msg-type-item {
+  margin: 10px;
+}
+
+.mst-list {
+  margin: 10px 0px;
+}
+
+.msg-detail-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.msg-content {
+  display: flex;
+  align-items: flex-start;
+  padding: 24px 16px;
+}
+
+.msg-icon {
+  flex-shrink: 0;
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20px;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.msg-icon i {
+  font-size: 28px;
+  color: #fff;
+}
+
+.msg-text {
+  flex: 1;
+}
+
+.msg-text h3 {
+  margin: 0 0 12px 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.msg-text p {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.msg-time {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+}
+
+.msg-time i {
+  margin-right: 4px;
+}
+
+.dialog-footer {
+  text-align: center;
+  padding: 10px 20px 20px;
+}
+
+.dialog-footer .el-button {
+  min-width: 100px;
+}
+</style>
+
+<style>
+.el-drawer__header {
+  margin-bottom: 10px;
+}
+
+.more {
+  text-align: center;
+  font-size: 14px;
+  color: #909399;
+}
+
+.msg-title {
+  margin: 10px 10px 20px 10px;
+  font-size: 16px;
+}
+
+.msg-detail-dialog .el-dialog__header {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 16px 20px;
+}
+
+.msg-detail-dialog .el-dialog__title {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.msg-detail-dialog .el-dialog__headerbtn .el-dialog__close {
+  color: #fff;
+  font-size: 18px;
+}
+
+.msg-detail-dialog .el-dialog__body {
+  padding: 0;
+}
+</style>