Procházet zdrojové kódy

feat: 前端集成websocket;新增指令语音通知

wanggaokun před 10 měsíci
rodič
revize
ded5519805

binární
src/assets/audio/tip.mp3


+ 2 - 2
src/layouts/components/Header/ToolBarRight.vue

@@ -2,11 +2,11 @@
   <div class="tool-bar-ri">
     <div class="header-icon">
       <SwitchDark />
+      <Message id="message" />
       <AssemblySize id="assemblySize" />
       <!-- <Language id="language" /> -->
       <SearchMenu id="searchMenu" />
       <ThemeSetting id="themeSetting" />
-      <!-- <Message id="message" /> -->
       <!-- <Fullscreen id="fullscreen" /> -->
     </div>
     <!-- <span class="username">{{ username }}</span> -->
@@ -21,7 +21,7 @@ import AssemblySize from './components/AssemblySize.vue'
 // import Language from './components/Language.vue'
 import SearchMenu from './components/SearchMenu.vue'
 import ThemeSetting from './components/ThemeSetting.vue'
-// import Message from './components/Message.vue'
+import Message from './components/Message.vue'
 // import Fullscreen from './components/Fullscreen.vue'
 import Avatar from './components/Avatar.vue'
 import SwitchDark from '@/components/SwitchDark/index.vue'

+ 66 - 40
src/layouts/components/Header/components/Message.vue

@@ -2,70 +2,91 @@
   <div class="message">
     <el-popover placement="bottom" :width="310" trigger="click">
       <template #reference>
-        <el-badge :value="5" class="item">
+        <el-badge v-if="newNotice > 0" :value="newNotice" class="item">
           <i :class="'iconfont icon-xiaoxi'" class="toolBar-icon"></i>
         </el-badge>
+        <i v-else :class="'iconfont icon-xiaoxi'" class="toolBar-icon"></i>
       </template>
       <el-tabs v-model="activeName">
-        <el-tab-pane label="通知(5)" name="first">
+        <el-tab-pane :label="`待办通知(${newNotice})`" name="first">
           <div class="message-list">
-            <div class="message-item">
-              <img src="@/assets/images/msg01.png" alt="" class="message-icon" />
-              <div class="message-content">
-                <span class="message-title">一键三连 Geeker-Admin 🧡</span>
-                <span class="message-date">一分钟前</span>
+            <template v-for="(item, index) in newsList" :key="index">
+              <div class="message-item">
+                <img src="@/assets/images/msg01.png" alt="" class="message-icon" />
+                <div class="message-content">
+                  <el-button link type="primary" @click="onNewsClick(index)">{{ item.message }}</el-button>
+                  <span class="message-date">{{ item.time }}</span>
+                </div>
+                <span v-if="item.read" class="el-tag el-tag--success el-tag--mini read">已读</span>
+                <span v-else class="el-tag el-tag--danger el-tag--mini read">未读</span>
               </div>
-            </div>
-            <div class="message-item">
-              <img src="@/assets/images/msg02.png" alt="" class="message-icon" />
-              <div class="message-content">
-                <span class="message-title">一键三连 Geeker-Admin 💙</span>
-                <span class="message-date">一小时前</span>
-              </div>
-            </div>
-            <div class="message-item">
-              <img src="@/assets/images/msg03.png" alt="" class="message-icon" />
-              <div class="message-content">
-                <span class="message-title">一键三连 Geeker-Admin 💚</span>
-                <span class="message-date">半天前</span>
-              </div>
-            </div>
-            <div class="message-item">
-              <img src="@/assets/images/msg04.png" alt="" class="message-icon" />
-              <div class="message-content">
-                <span class="message-title">一键三连 Geeker-Admin 💜</span>
-                <span class="message-date">一星期前</span>
-              </div>
-            </div>
-            <div class="message-item">
-              <img src="@/assets/images/msg05.png" alt="" class="message-icon" />
-              <div class="message-content">
-                <span class="message-title">一键三连 Geeker-Admin 💛</span>
-                <span class="message-date">一个月前</span>
-              </div>
-            </div>
+            </template>
           </div>
         </el-tab-pane>
-        <el-tab-pane label="消息(0)" name="second">
+        <!-- <el-tab-pane label="消息(0)" name="second">
           <div class="message-empty">
             <img src="@/assets/images/notData.png" alt="notData" />
             <div>暂无消息</div>
           </div>
         </el-tab-pane>
-        <el-tab-pane label="办(0)" name="third">
+        <el-tab-pane label="待办(0)" name="third">
           <div class="message-empty">
             <img src="@/assets/images/notData.png" alt="notData" />
             <div>暂无代办</div>
           </div>
-        </el-tab-pane>
+        </el-tab-pane> -->
       </el-tabs>
     </el-popover>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { storeToRefs } from 'pinia'
+import { reactive, ref, onMounted, watch, nextTick } from 'vue'
+import useNoticeStore from '@/stores/modules/notice'
+import { useRouter } from 'vue-router'
+const router = useRouter()
+const noticeStore = storeToRefs(useNoticeStore())
+// const { readAll } = useNoticeStore()
 const activeName = ref('first')
+const newNotice = ref(0)
+// 定义变量内容
+const state = reactive({
+  loading: false
+})
+const newsList = ref([]) as any
+//点击消息,写入已读
+const onNewsClick = (item: any) => {
+  newsList.value[item].read = true
+  //并且写入pinia
+  noticeStore.state.value.notices = newsList.value
+  router.push({ path: `/index` }).then(() => {
+    nextTick(() => {
+      window.location.reload()
+    })
+  })
+}
+/**
+ * 初始化数据
+ * @returns
+ */
+const getTableData = async () => {
+  state.loading = true
+  newsList.value = noticeStore.state.value.notices
+  state.loading = false
+}
+
+onMounted(() => {
+  nextTick(() => {
+    getTableData()
+  })
+})
+//用深度监听 消息
+watch(
+  () => noticeStore.state.value.notices,
+  newVal => (newNotice.value = newVal.filter((item: any) => !item.read).length),
+  { deep: true }
+)
 </script>
 
 <style scoped lang="scss">
@@ -80,6 +101,7 @@ const activeName = ref('first')
 .message-list {
   display: flex;
   flex-direction: column;
+  min-height: 260px;
   .message-item {
     display: flex;
     align-items: center;
@@ -92,10 +114,14 @@ const activeName = ref('first')
       width: 40px;
       height: 40px;
       margin: 0 20px 0 5px;
+      .img {
+        cursor: pointer;
+      }
     }
     .message-content {
       display: flex;
       flex-direction: column;
+      margin-right: 2px;
       .message-title {
         margin-bottom: 5px;
       }

+ 0 - 1
src/layouts/index.vue

@@ -13,7 +13,6 @@ import LayoutVertical from './LayoutVertical/index.vue'
 import LayoutClassic from './LayoutClassic/index.vue'
 import LayoutTransverse from './LayoutTransverse/index.vue'
 import LayoutColumns from './LayoutColumns/index.vue'
-
 const LayoutComponents: Record<LayoutType, Component> = {
   vertical: LayoutVertical,
   classic: LayoutClassic,

+ 42 - 0
src/stores/modules/notice.ts

@@ -0,0 +1,42 @@
+import { reactive } from 'vue'
+import { defineStore } from 'pinia'
+interface NoticeItem {
+  title?: string
+  read: boolean
+  message: any
+  time: string
+}
+
+export const useNoticeStore = defineStore('notice', () => {
+  const state = reactive({
+    notices: [] as NoticeItem[]
+  })
+
+  const addNotice = (notice: NoticeItem) => {
+    state.notices.push(notice)
+  }
+
+  const removeNotice = (notice: NoticeItem) => {
+    state.notices.splice(state.notices.indexOf(notice), 1)
+  }
+
+  //实现全部已读
+  const readAll = () => {
+    state.notices.forEach((item: any) => {
+      item.read = true
+    })
+  }
+
+  const clearNotice = () => {
+    state.notices = []
+  }
+  return {
+    state,
+    addNotice,
+    removeNotice,
+    readAll,
+    clearNotice
+  }
+})
+
+export default useNoticeStore

+ 148 - 0
src/utils/websocket.ts

@@ -0,0 +1,148 @@
+/**
+ * @module initWebSocket 初始化
+ * @module websocketOnopen 连接成功
+ * @module websocketOnerror 连接失败
+ * @module websocketClose 断开连接
+ * @module resetHeart 重置心跳
+ * @module sendSocketHeart 心跳发送
+ * @module reconnect 重连
+ * @module sendMsg 发送数据
+ * @module websocketOnmessage 接收数据
+ * @module test 测试收到消息传递
+ * @description socket 通信
+ * @param {any} url socket地址
+ * @param {any} websocket websocket 实例
+ * @param {any} heartTime 心跳定时器实例
+ * @param {number} socketHeart 心跳次数
+ * @param {number} HeartTimeOut 心跳超时时间
+ * @param {number} socketError 错误次数
+ */
+
+// import { getToken } from '@/utils/token'
+import { ElNotification } from 'element-plus'
+import useNoticeStore from '@/stores/modules/notice'
+let socketUrl: any = '' // socket地址
+let websocket: any = null // websocket 实例
+let heartTime: any = null // 心跳定时器实例
+let socketHeart = 0 as number // 心跳次数
+const HeartTimeOut = 10000 // 心跳超时时间 10000 = 10s
+let socketError = 0 as number // 错误次数
+// import { getTimeState } from '@/utils'
+// 初始化socket
+export const initWebSocket = (url: any) => {
+  if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
+    return
+  }
+  socketUrl = url
+  // 初始化 websocket
+  websocket = new WebSocket(url)
+  websocketOnopen()
+  websocketOnmessage()
+  websocketOnerror()
+  websocketClose()
+  sendSocketHeart()
+  return websocket
+}
+
+// socket 连接成功
+export const websocketOnopen = () => {
+  websocket.onopen = function () {
+    console.log('连接 websocket 成功')
+    resetHeart()
+  }
+}
+
+// socket 连接失败
+export const websocketOnerror = () => {
+  websocket.onerror = function (e: any) {
+    console.log('连接 websocket 失败', e)
+  }
+}
+
+// socket 断开链接
+export const websocketClose = () => {
+  websocket.onclose = function (e: any) {
+    console.log('断开连接', e)
+  }
+}
+
+// socket 重置心跳
+export const resetHeart = () => {
+  socketHeart = 0
+  socketError = 0
+  clearInterval(heartTime)
+  sendSocketHeart()
+}
+
+// socket心跳发送
+export const sendSocketHeart = () => {
+  heartTime = setInterval(() => {
+    // 如果连接正常则发送心跳
+    if (websocket.readyState == 1) {
+      // if (socketHeart <= 30) {
+      websocket.send(
+        JSON.stringify({
+          type: 'ping'
+        })
+      )
+      socketHeart = socketHeart + 1
+    } else {
+      // 重连
+      reconnect()
+    }
+  }, HeartTimeOut)
+}
+
+// socket重连
+export const reconnect = () => {
+  if (socketError <= 2) {
+    clearInterval(heartTime)
+    initWebSocket(socketUrl)
+    socketError = socketError + 1
+    // eslint-disable-next-line prettier/prettier
+    console.log('socket重连', socketError)
+  } else {
+    // eslint-disable-next-line prettier/prettier
+    console.log('重试次数已用完')
+    clearInterval(heartTime)
+  }
+}
+
+// socket 发送数据
+export const sendMsg = (data: any) => {
+  websocket.send(data)
+}
+
+const playSound = () => {
+  const audio = new Audio('src/assets/audio/tip.mp3')
+  audio.play()
+}
+
+// socket 接收数据
+export const websocketOnmessage = () => {
+  websocket.onmessage = function (e: any) {
+    let title = '消息'
+    if (e.data.indexOf('heartbeat') > 0) {
+      resetHeart()
+    }
+    if (e.data.indexOf('ping') > 0) {
+      return
+    }
+    if (e.data.indexOf('连接成功') >= 0) {
+      return
+    }
+    useNoticeStore().addNotice({
+      message: e.data,
+      read: false,
+      time: new Date().toLocaleString()
+    })
+    ElNotification({
+      title,
+      message: e.data,
+      type: 'success',
+      duration: 3000
+    })
+    playSound()
+    return e.data
+  }
+}

+ 5 - 0
src/views/login/components/LoginForm.vue

@@ -48,6 +48,8 @@ import type { ElForm } from 'element-plus'
 import { lodashFunc } from '@/utils/common'
 import Cookies from 'js-cookie'
 import { encrypt, decrypt } from '@/utils/jsEncrypt'
+import { initWebSocket } from '@/utils/websocket'
+
 const router = useRouter()
 const userStore = useUserStore()
 const tabsStore = useTabsStore()
@@ -104,6 +106,9 @@ const handleLogin = (formEl: FormInstance | undefined) => {
           // 4.跳转到首页
           router.push(HOME_URL)
           loading.value = false
+          let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'
+          // initWebSocket(protocol + 'localhost:8089' + '/resource/websocket')
+          initWebSocket(protocol + window.location.host + import.meta.env.VITE_API_URL + '/websocket/message/' + loginForm.username)
         })
         .catch(error => {
           console.log('登录失败', error)

+ 1 - 2
src/views/manage/orderInfo/index.vue

@@ -21,7 +21,6 @@
 import { ref, reactive } from 'vue'
 import { useDownload } from '@/hooks/useDownload'
 import { ElMessageBox, ElMessage } from 'element-plus'
-import { useRouter } from 'vue-router'
 import ProTable from '@/components/ProTable/index.vue'
 import FormDialog from '@/components/DialogOld/form.vue'
 import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
@@ -29,7 +28,7 @@ import { Download } from '@element-plus/icons-vue'
 import { listOrderInfoApi, exportApi } from '@/api/modules/manage/orderInfo'
 import { getOrderConfigByCodeApi } from '@/api/modules/manage/orderConfig'
 import { getDictsApi } from '@/api/modules/system/dictData'
-
+import { useRouter } from 'vue-router'
 const router = useRouter()
 
 // 跳转执行