首页 > 其他分享 >VUE +WebSocket+speak-tt 实现在浏览器右下角实时给商家推送订单消息

VUE +WebSocket+speak-tt 实现在浏览器右下角实时给商家推送订单消息

时间:2025-01-09 13:43:54浏览次数:1  
标签:function VUE websocket log tt session WebSocket message evt

先看效果

 

 1、WebSocket服务建立

 1.1 引入包

 

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

1.2 新建配置类

package com.ruoyi.web.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


@Configuration
public class WebSocketConfig {

    /**
     * ServerEndpointExporter 作用
     * <p>
     * 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

1.3 新建服务类

 

package com.ruoyi.web.core.websocket.service;

import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * websocket核心代码
 * // 接口路径 ws://localhost:8080/webSocket/userId;
 */
@Component
@Slf4j
@ServerEndpoint("/webSocket/{userId}")
public class WebSocketServer {

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 用户ID
     */
    private String userId;
    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     * 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
     * 注:底下WebSocket是当前类名
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
    /**
     * 用来存在线连接用户信息
     */
    private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();

    private static ApplicationContext applicationContext;

    public static void setApplicationContext(ApplicationContext context) {
        applicationContext = context;
    }

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
        try {
            this.session = session;
            this.userId = userId;
            webSockets.add(this);
            sessionPool.put(userId, session);
            log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            webSockets.remove(this);
            sessionPool.remove(this.userId);
            log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("【websocket消息】收到客户端消息:" + message);
//        JSONObject jsonObject = JSONObject.parseObject(message);
    }

    /**
     * 发送错误时的处理
     *
     * @param session
     * @param error
     */
    @OnError
    public void one rror(Session session, Throwable error) {
        log.error("用户错误,原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 此为广播消息
     *
     * @param message
     */
    public void sendAllMessage(String message) {
        log.info("【websocket消息】广播消息:" + message);
        for (WebSocketServer webSocket : webSockets) {
            try {
                if (webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 此为单点消息
     *
     * @param userId
     * @param message
     */
    public void sendOneMessage(String userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("【websocket消息】 单点消息:" + message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 此为单点消息(多人)
     *
     * @param userIds
     * @param message
     */
    public void sendMoreMessage(String[] userIds, String message) {
        for (String userId : userIds) {
            Session session = sessionPool.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【websocket消息】 单点消息:" + message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

1.4 WebSocket 服务端已经完成,启动项目测试以下是否正常,下面顺便放一个html网页版测试工具

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>wsClient</title>
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <style>
        .btn-group{
            display: inline-block;
        }
    </style>
</head>
<body>
<input type='text' value='ws://localhost:8080/webSocket/userId' class="form-control" style='width:390px;display:inline'
       id='wsaddr' />
<div class="btn-group" >
    <button type="button" class="btn btn-default" onclick='addsocket();'>连接</button>
    <button type="button" class="btn btn-default" onclick='closesocket();'>断开</button>
    <button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button>
    <button type="button" class="btn btn-default" onclick='restore()'>还原</button>
</div>
<div id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin: 20px 0;"></div>
    <input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);">
    <span class="input-group-btn">
        <button class="btn btn-default" type="button" onclick="doSend();">发送</button>
    </span>
</div>

<script>
    /*组织时间*/
    function formatDate(now) {
        var year = now.getFullYear();
        var month = now.getMonth() + 1;
        var date = now.getDate();
        var hour = now.getHours();
        var minute = now.getMinutes();
        var second = now.getSeconds();
        return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) +
            " " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (
                second = second < 10 ? ("0" + second) : second);
    }
    var output;
    var websocket;

    function init() {
        output = document.getElementById("output");
    }

    /*连接按钮*/
    function addsocket() {
        var wsaddr = $("#wsaddr").val();
        if (wsaddr == '') {
            alert("请填写websocket的地址");
            return false;
        }
        StartWebSocket(wsaddr);
    }

    /*断开按钮*/
    function closesocket() {
        websocket.close();
    }

    /*还原按钮*/
    function restore(){
        $("#wsaddr").val('ws://192.168.0.154:8080/');
    }

    function en(event) {
        var evt = evt ? evt : (window.event ? window.event : null);
        if (evt.keyCode == 13) {
            doSend()
        }
    }

    /*发送按钮*/
    function doSend() {
        var message = $("#message").val();
        if (message == '') {
            alert("请先填写发送信息");
            $("#message").focus();
            return false;
        }
        if (typeof websocket === "undefined") {
            alert("websocket还没有连接,或者连接失败,请检测");
            return false;
        }
        if (websocket.readyState == 3) {
            alert("websocket已经关闭,请重新连接");
            return false;
        }
        console.log(websocket);
        $("#message").val('');
        writeToScreen('<span style="color:green">你发送的信息&nbsp;' + formatDate(new Date()) + '</span><br/>' + message);
        websocket.send(message);
    }

    /*书写内容*/
    function StartWebSocket(wsUri) {
        websocket = new WebSocket(wsUri);
        websocket.onopen = function(evt) {
            onOpen(evt)
        };
        websocket.onclose = function(evt) {
            onClose(evt)
        };
        websocket.onmessage = function(evt) {
            onMessage(evt)
        };
        websocket.onerror = function(evt) {
            one rror(evt)
        };
    }

    function onOpen(evt) {
        writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");
    }

    function onClose(evt) {
        writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");
        websocket.close();
    }

    function onMessage(evt) {
        writeToScreen('<span style="color:blue">服务端回应&nbsp;' + formatDate(new Date()) + '</span><br/><span class="bubble">' +
            evt.data + '</span>');
    }

    function one rror(evt) {
        writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data);
    }

    function writeToScreen(message) {
        var div = "<div class='newmessage'>" + message + "</div>";
        var d = $("#output");
        var d = d[0];
        var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
        $("#output").append(div);
        if (doScroll) {
            d.scrollTop = d.scrollHeight - d.clientHeight;
        }
    }


</script>
</body>
</html>

 2、Vue 前端实现

2.1 首先项目中安装speak-tts语音播报插件

 

npm install speak-tts

2.2创建一个全局的 speech.js文件,文件中引入插件并初始化后导出。因为可能会一直读多条消息,防止初始化多个Speech对象,在全局api中初始化一个对象,方便播报的时候调用。

import Speech from 'speak-tts'
const speech=new Speech()
export default speech 

2.3 在项目点击登录按钮后调用全局的语音播报方法。
由于浏览器之间有安全限制,用户不主动触发语音播报方法, 语音播报不会主动发出声音,故在项目的登录处触发方法。

2.3.1 在登录页面 引入封装好得js文件,并初始化方法

import Speech from '@/utils/speech'


 initSpeech(){
      Speech.setLanguage('zh-CN')
      Speech.init({
        volume: 0.6, // 音量0-1
        lang: "zh-CN", // 语言
        rate: 2, // 语速1正常语速,2倍语速就写2
        pitch: 1, // 音调
        voice: "Microsoft Yaoyao - Chinese (Simplified, PRC)",
      })    
    }

3 在登录后的页面入口文件处编写弹框样式及告警信息的接收等功能(我项目是elementUI 所以是AppMain.vue 页面 在layout下面)

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <keep-alive :include="cachedViews">
        <router-view v-if="!$route.meta.link" :key="key" />
      </keep-alive>
    </transition>
    <iframe-toggle />
    <div class="alarmmodel" v-if="popupList.length > 0">
      <el-card
        class="box-card"
        shadow="always"
        v-for="(item, index) in popupList"
        :key="index"
      >
        <div slot="header" class="clearfix">
          <span
            style="color: green; font-size: 25px"
            v-if="item.notifyType == 'newOrder'"
            >{{ index + 1 }}、订单提醒</span
          >
          <span
            style="color: red; font-size: 25px"
            v-else-if="item.notifyType == 'refundOrder'"
            >{{ index + 1 }}、取消订单提醒</span
          >
          <el-button
            style="float: right; padding: 3px 0"
            type="text"
            v-if="item.notifyType == 'newOrder'"
            @click="popupSubmit(item, index, item.notifyType)"
            >接单</el-button
          >
          <el-button
            style="float: right; padding: 3px 0"
            type="text"
            v-else
            @click="popupSubmit(item, index, item.notifyType)"
            >确定</el-button
          >
        </div>
        <div class="orderInfo">
          <p>
            <span class="orderInfo_title">名 称:</span> {{ item.goodsName }}
          </p>
          <p><span class="orderInfo_title">数 量:</span> {{ item.goodsNum }}</p>
          <p>
            <span class="orderInfo_title">金 额:</span> {{ item.payPrice }} 元
          </p>
          <p><span class="orderInfo_title">备 注:</span> {{ item.remark }}</p>
        </div>
      </el-card>
    </div>
  </section>
</template>

<script>
import iframeToggle from "./IframeToggle/index";
import Speech from "@/utils/speech";
import {
  updateOrderStatus
} from "@/api/orderInfo";

export default {
  name: "AppMain",
  components: { iframeToggle },
  data() {
    return {
      heartbeatTimer: null, // 监测心跳
      popupList: [], // 存储弹框数据
      pathpopup: window._CONFIG['WebSocketUrl'], // websocket链接地址
      socketpopup: null, // 初始化websocket对象
    };
  },
  computed: {
    cachedViews() {
      return this.$store.state.tagsView.cachedViews;
    },
    key() {
      return this.$route.path;
    },
  },
  mounted() {
    this.popupList = [];
    this.initPopupsoket();
  },
  beforeDestroy() {
    this.speech.cancel(); // 取消播放
    this.speech = null;
    this.socketpopup.onclose = this.closePopup;
  },
  methods: {
    initPopupsoket() {
      if (typeof WebSocket === "undefined") {
        alert("您的浏览器不支持socket");
      } else {
        //获取当前登录用户ID
        const uid = this.$store.getters.id;
        console.log("当前登录用户信息", uid);
        if (uid == undefined) {
          console.log("未获取到商家用户ID,无法实时推送订单消息");
        } else {
          this.socketpopup = new WebSocket(this.pathpopup + uid);
          this.socketpopup.onopen = this.openPopup;
          this.socketpopup.onerror = this.errorPopup;
          this.socketpopup.onmessage = this.getMessagepopup;
        }
      }
    },
    openPopup() {
      console.log("socketpopup连接成功");
      this.startHeartbeat(); // 添加心跳监测,用来防止websocket断开
    },
    startHeartbeat() {
      // 发送心跳消息
      var _this = this;
      if (_this.heartbeatTimer == null) {
        _this.heartbeatTimer = setInterval(function () {
          console.log("监测心跳");
          _this.socketpopup.send("ping");
        }, 10000);
      }
    },
    stopHeartbeat() {
      // 停止心跳
      if (this.heartbeatTimer !== null) {
        clearInterval(this.heartbeatTimer);
        this.heartbeatTimer = null;
      }
    },
    errorPopup() {
      console.log("1连接错误");
    },
    getMessagepopup(msg) {
      const returnMsg = JSON.parse(msg.data);
      console.log("接受消息数据1", returnMsg);
      console.log("接受消息数据2", returnMsg.message);
      this.popupList.push(returnMsg); // 将推送的单条数据存起来显示多个弹框,在用户点击确定后消除此条弹框
      this.startSpeech(returnMsg.message); // 将数据中的告警传给播报的对象
    },
    closePopup() {
      console.log("socketpopup已经关闭");
      //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
      window.onbeforeunload = function () {
        this.socketpopup.close();
      };
    },
    startSpeech(text) {
      Speech.speak({
        text: text,
        listeners: {
          //开始播放
          onstart: () => {
            console.log("Start utterance");
          },
          //判断播放是否完毕
          onend: () => {
            console.log("End utterance");
          },
          //恢复播放
          onresume: () => {
            console.log("Resume utterance");
          },
        },
      }).then(() => {
        console.log("读取成功", this.popupList.length);
      });
    },
    popupSubmit(item, index, notifyType) {
      if (notifyType == "newOrder") {
        //接单
        updateOrderStatus({ id: item.orderId, orderStatus: '4' }).then((response) => {
          this.$message.success("接单成功!");
          this.popupList.splice(index, 1);
          this.$router.push({ path: "/order_manage/dl_order_info" }).catch(() => {});
      });
       
      } else {
        this.popupList.splice(index, 1);
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.app-main {
  /* 50= navbar  50  */
  min-height: calc(100vh - 50px);
  width: 100%;
  position: relative;
  overflow: hidden;
}

.fixed-header + .app-main {
  padding-top: 50px;
}

.hasTagsView {
  .app-main {
    /* 84 = navbar + tags-view = 50 + 34 */
    min-height: calc(100vh - 84px);
  }

  .fixed-header + .app-main {
    padding-top: 84px;
  }
}
</style>

<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
  .fixed-header {
    padding-right: 6px;
  }
}

::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

::-webkit-scrollbar-track {
  background-color: #f1f1f1;
}

::-webkit-scrollbar-thumb {
  background-color: #c0c0c0;
  border-radius: 3px;
}

.alarmmodel {
  position: fixed; /* 使div固定在页面上的某个位置 */
  bottom: 10px; /* 距离顶部10像素 */
  right: 10px; /* 距离右侧10像素 */
  z-index: 1000; /* 确保div在其他内容之上 */
  width: 280px; /* 弹窗宽度 */
  height: 400px; /* 弹窗高度 */
  overflow-y: scroll;
  background-color: rgb(201, 194, 194);
  color: #fff; /* 文字颜色 */
  text-align: center; /* 文字居中 */
}
.orderInfo {
  text-align: left;
}
.orderInfo_title {
  font-weight: bolder;
}
</style>

 

标签:function,VUE,websocket,log,tt,session,WebSocket,message,evt
From: https://www.cnblogs.com/bin521/p/18662000

相关文章

  • Vue 的 transition 组件
    在开发名为USV项目时,特别是H5页面的项目,还有一个组件是我们非常常用的,它相对弹框来说没有那么大,并且不需要手动关闭在需要更简洁的提示用户一些信息时非常常用,它就是toast提示组件;接下来我们会带着大家手写一个全局的toast提示组件,当你在项目任何地方需要使用时,都可直接调用......
  • 一个个顺序挨着来 - 责任链模式(Chain of Responsibility Pattern)
    责任链模式(ChainofResponsibilityPattern)责任链模式(ChainofResponsibilityPattern)责任链模式(ChainofResponsibilityPattern)概述责任链结构图责任链模式概述责任链模式涉及的角色talkischeap,showyoumycode总结责任链模式(ChainofResponsibilityPatt......
  • 前端必知必会-Node.js HTTP 模块
    文章目录Node.jsHTTP模块内置HTTP模块Node.js作为Web服务器添加HTTP标头读取查询字符串拆分查询字符串总结Node.jsHTTP模块内置HTTP模块Node.js有一个名为HTTP的内置模块,它允许Node.js通过超文本传输​​协议(HTTP)传输数据。要包含HTTP模......
  • Vue3 ref函数 数据响应式
    1、作用:定义响应式数据2、语法a、创建创建一个包含响应式数据的引用对象letxx=ref(数据)b、JS操作xx.valuec、模板操作{{xx}}3、注意数据可以是:基本类型,也可以是对象类型基本类型需要.value获取值,对象中的数据不需要案例<template><h2>姓名:{{name}}</h2>......
  • Vue3 setup
    1、setup是一个函数2、组件中所用到的:数据、方法等,均要配置在setup中3、setup函数返回值(两种)a、返回对象,则对象中的属性、方法等在模板中可以直接使用案例<template><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><button@click="showMessage">点击</button></tem......
  • vue 新增编辑的时候性别下拉框展示数据和列表展示对应的男和女
         相关代码<el-form-itemlabel="员工性别"><el-selectv-model="form.gender"placeholder="请选择性别"><el-optionv-for="dictindict.type.sys_user_sex"......
  • Python+Django鹿幸公司员工在线餐饮管理系统的设计与实现(Pycharm Flask Django Vue m
    收藏关注不迷路,防止下次找不到!文章末尾有惊喜项目介绍Python+Django鹿幸公司员工在线餐饮管理系统的设计与实现(PycharmFlaskDjangoVuemysql)项目展示详细视频演示请联系我获取更详细的演示视频,相识就是缘分,欢迎合作!!!所用技术栈前端vue.js框......
  • Python+Django高校网上缴费综合务系统(Pycharm Flask Django Vue mysql)
    收藏关注不迷路,防止下次找不到!文章末尾有惊喜项目介绍Python+Django高校网上缴费综合务系统(PycharmFlaskDjangoVuemysql)项目展示详细视频演示请联系我获取更详细的演示视频,相识就是缘分,欢迎合作!!!所用技术栈前端vue.js框架支持:django数据库:mysql5.7数......
  • avue 长表单校验自动定位到错误位置
    前言在使用avue时有时候需要用到很长的表单,长表单保存提交且有校验错误时,如果错误位置不在滚动屏幕可视区域,此时用户看不到任何提示信息,用户需要去滚动寻找哪一项校验不通过,用户体验很不好,需要自动定位到错误位置。解决办法avue官方文档中,avue-form和avue-crud的错误回调......
  • 【行空板K10】MQTT Plus用户库:对Mind+的MQTT功能进行增强
    目录引言Mind+MQTT功能实现的分析功能增强对Clientid的支持对保留消息的支持用户库的编写基本结构config.jsonmain.tslibraries示例程序巴法云华为云结语本文首发于DFRobot论坛:MQTTPlus用户库:对Mind+的MQTT功能进行增强DF创客社区。引言前面的博文介绍......