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

news/2025/1/9 13:42:58/文章来源:https://www.cnblogs.com/bin521/p/18662000

先看效果

 

 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*/@Beanpublic 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;}/*** 链接成功调用的方法*/@OnOpenpublic 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) {}}/*** 链接关闭调用的方法*/@OnClosepublic void onClose() {try {webSockets.remove(this);sessionPool.remove(this.userId);log.info("【websocket消息】连接断开,总数为:" + webSockets.size());} catch (Exception e) {}}/*** 收到客户端消息后调用的方法** @param message*/@OnMessagepublic void onMessage(String message) {log.info("【websocket消息】收到客户端消息:" + message);
//        JSONObject jsonObject = JSONObject.parseObject(message);
    }/*** 发送错误时的处理** @param session* @param error*/@OnErrorpublic void onError(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) {onError(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 onError(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-1lang: "zh-CN", // 语言rate: 2, // 语速1正常语速,2倍语速就写2pitch: 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-cardclass="box-card"shadow="always"v-for="(item, index) in popupList":key="index"><div slot="header" class="clearfix"><spanstyle="color: green; font-size: 25px"v-if="item.notifyType == 'newOrder'">{{ index + 1 }}、订单提醒</span><spanstyle="color: red; font-size: 25px"v-else-if="item.notifyType == 'refundOrder'">{{ index + 1 }}、取消订单提醒</span><el-buttonstyle="float: right; padding: 3px 0"type="text"v-if="item.notifyType == 'newOrder'"@click="popupSubmit(item, index, item.notifyType)">接单</el-button><el-buttonstyle="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 {//获取当前登录用户IDconst 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>

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/866536.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASE65R180-ASEMI超洁MOS管ASE65R180

ASE65R180-ASEMI超洁MOS管ASE65R180编辑:ll ASE65R180-ASEMI超洁MOS管ASE65R180 型号:ASE100N10 品牌:ASEMI 封装:TO-220F 批号:最新 最大漏源电流:21A 漏源击穿电压:650V RDS(ON)Max:180mΩ 引脚数量:3 芯片个数: 沟道类型:P沟道MOS管、超洁MOS管 漏电流:ua 特性…

【搜索】DFS与BFS

99. 岛屿数量讲解:https://programmercarl.com/kamacoder/0099.岛屿的数量广搜.html#思路 DFS代码 #include <iostream> #include <cstring>using namespace std;const int N = 55;int n, m; int g[N][N]; bool st[N][N]; int dx[4] = {-1, 0, 1, 0}, dy[4] = {0,…

DFS与BFS专题

99. 岛屿数量讲解:https://programmercarl.com/kamacoder/0099.岛屿的数量广搜.html#思路 DFS代码 #include <iostream> #include <cstring>using namespace std;const int N = 55;int n, m; int g[N][N]; bool st[N][N]; int dx[4] = {-1, 0, 1, 0}, dy[4] = {0,…

Spinnaker

Spinnaker 是一个持续交付平台,它定位于将产品快速且持续的部署到多种云平台上。 Spinnaker 主要特性:配置一次,随时运行;随地部署,集中化管理;开源。 Spinnaker 组件:Spinnaker 最初是以实现内部的端到端持续交付为目标,作为 Asgard 的替代,该项目期望重建一个持续交…

Java基础学习(五)

Java基础学习(五):数组 目录Java基础学习(五):数组概念声明与创建初始化基本特点内存分析应用多维数组扩展内容Arrays 类冒泡排序稀疏数组 本文为个人学习记录,内容学习自 狂神说Java概念数组是相同类型数据的有序集合 每个数据称为一个数组元素,可以通过下标来访问声明…

CDS标准视图:付款锁定原因 I_PaymentBlockingReason

视图名称:付款锁定原因 I_PaymentBlockingReason 视图类型:基础视图 视图代码:点击查看代码 //Documentation about annotations can be found at http://help.sap.com searching for CDS annotations //Inserted by VDM CDS Suite Plugin @ObjectModel.usageType.sizeCateg…

欧拉OpenEuler使用nfs和rsync复制文件夹到新服务器.250109

案例: 服务器A是新服务器 服务器B为老服务器 需要将服务器B的/data/storage ,拷贝到服务器A的 /home/sync-data下一、服务器A 新服务器配置nfs 1. 安装nfs systemctl stop firewalld df -h mkdir -p /home/sync-datayum install nfs-utils systemctl status nfs-se…

在 .NET 9 中使用 Scalar 替代 Swagger

前言 在.NET 9发布以后ASP.NET Core官方团队发布公告已经将Swashbuckle.AspNetCore(一个为ASP.NET Core API提供Swagger工具的项目)从ASP.NET Core Web API模板中移除,这意味着以后我们创建Web API项目的时候不会再自动生成Swagger API文档了。那么今天咱们一起来试试把我们…

第九章 范围管理 (2025年详细解析版)

目录什么是范围管理?9.1 管理基础9.1.1 产品范围和项目范围9.1.2 产品范围和项目范围管理新实践9.2 项目范围管理过程9.2.1 过程概述9.2.2 项目范围管理过程9.2.3 裁剪考虑因素(了解)9.2.4 敏捷与适应方法总结9.3 规划范围管理1. 课程目标2. 过程定义:规划范围管…

Qt监控系统远程网络登录/请求设备列表/服务器查看实时流/回放视频/验证码请求

一、前言说明 这几个功能是近期定制的功能,也非常具有代表性,核心就是之前登录和设备信息都是在本地,存放在数据库中,数据库可以是本地或者远程的,现在需要改成通过网络API请求的方式,现在很多的服务器很强大,都提供了各种API接口,包括登录和拉取回放等,相当于直接对接…

debian10测试

https://help.aliyun.com/zh/ecs/user-guide/change-debian-9-or-10-repository-addresses?spm=a2c4g.11186623.0.0.52c44bccrP9uFq

C-V2X测试探秘系列之:电磁兼容试验方法

在智能网联汽车中,各种通信技术如2G/3G/4G/5G、GNSS(全球导航卫星系统)、V2X(车联网通信)等在行业内被广泛使用。这些技术让汽车能够实现紧急呼叫、在线娱乐、导航等多种功能。EMC测试就是为了确保在复杂电磁环境下,汽车的通信系统仍然可以正常工作,保护驾乘者的安全。参…