ChatRobot.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <template>
  2. <div class="chat-right-header-container">
  3. {{selectChatUser.userName}}
  4. </div>
  5. <div class="chat-core-container " ref="chatCoreContainerRef">
  6. <div v-for="chatMessage in chatMessageArr"
  7. :class="['chat-message-card',
  8. chatMessage.senderId === loginUser.userId ? 'my-message' : 'other-message']">
  9. <!-- 自己的消息,头像在右,消息在左 -->
  10. <template v-if="chatMessage.senderId === loginUser.userId">
  11. <div class="message-content my-message-content" :style="{ backgroundColor: '#a9ea7a' }">
  12. {{ chatMessage.message }}
  13. </div>
  14. <div class="message-avatar my-avatar">
  15. <img :src="loginUser.photo" alt="avatar" />
  16. <div class="chat-mess-triangle-right"></div>
  17. </div>
  18. </template>
  19. <!-- 其他人的消息,头像在左,消息在右 -->
  20. <template v-else>
  21. <div class="message-avatar other-avatar">
  22. <img :src="selectChatUser.photo" alt="avatar" />
  23. <div class="chat-mess-triangle-left"></div>
  24. </div>
  25. <div v-html="md.render(chatMessage.message)" class="message-content other-message-content" :style="{ backgroundColor: '#f5f5f5' }"></div>
  26. </template>
  27. </div>
  28. </div>
  29. <div class="chat-editer-menu-container">
  30. 提示: 机器人基于大模型实现,支持对话和文生图, 可以试试输入:"数据库索引介绍" 或 "帮我画一只小猫咪"
  31. </div>
  32. <div class="chat-editer-container">
  33. <textarea
  34. ref="inputRef"
  35. v-model="inputValue"
  36. placeholder="请输入消息..."
  37. :auto-focus="true"
  38. @input="handleInput"
  39. @keydown="handleSendMessage"
  40. class="chat-input"
  41. ></textarea>
  42. </div>
  43. </template>
  44. <script>
  45. import { inject, defineComponent, ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
  46. import { message } from 'ant-design-vue';
  47. import axios from 'axios';
  48. import MarkdownIt from 'markdown-it'
  49. import hljs from'highlight.js';
  50. export default defineComponent({
  51. props: {
  52. selectChatUserRef: Object
  53. },
  54. setup(props) {
  55. const inputValue = ref('');
  56. const socket = ref(null);
  57. const eventSource = ref(null);
  58. const messageQueue = ref([]); // 存储未发送的消息队列
  59. const isConnected = ref(false);
  60. const loginUser = ref(inject('loginUser'));
  61. const selectChatUser = ref(props.selectChatUserRef);
  62. const reconnectInterval = ref(5000); // 重连间隔时间,单位:毫秒
  63. const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
  64. const chatMessageArr = ref([]);
  65. const running = ref(false);
  66. const md = new MarkdownIt();
  67. const robotMessage = ref('');
  68. const chatCoreContainerRef = ref(null);
  69. const loadChatMessages = (chatUserId) => {
  70. // 先清空
  71. axios.get(`/client-service/chat/message/list/`+chatUserId, {})
  72. .then(function (response) {
  73. if (response.data.code === 40001) {
  74. // 未登录
  75. window.location.href = response.data.redirect_url;
  76. } else if (response.data.code === 200) {
  77. if (response.data.data.length > 0) {
  78. chatMessageArr.value = response.data.data;
  79. }
  80. }
  81. }).catch(function (error) {
  82. // 处理错误
  83. console.error(error);
  84. });
  85. }
  86. loadChatMessages( selectChatUser.value.userId);
  87. // 切换对话用户,父组件传递选中用户信息,监听选中用户变化
  88. watch(() => props.selectChatUserRef, (newValue) => {
  89. selectChatUser.value = newValue;
  90. loadChatMessages(selectChatUser.value.userId);
  91. });
  92. // SSE链接,用户机器人对话
  93. const connectSSE = () => {
  94. eventSource.value = new EventSource(`/aigc-service/event/stream?bizCode=PC_WEB_CHAT&userId=`+loginUser.value.userId);
  95. eventSource.value.onmessage = (event) => {
  96. // running.value = true;
  97. robotMessage.value += event.data;
  98. if (event.data == 'finished') {
  99. running.value = false;
  100. // saveRobotMessage(systemMessage);
  101. robotMessage.value = '';
  102. } else {
  103. const lastRobotMessage = chatMessageArr.value[chatMessageArr.value.length - 1];
  104. if (lastRobotMessage && lastRobotMessage.senderId === selectChatUser.value.userId) {
  105. // 使用 Vue 的响应式更新
  106. lastRobotMessage.message = robotMessage.value;
  107. }
  108. // 滚动条到底部
  109. }
  110. };
  111. eventSource.value.onerror = (error) => {
  112. if (eventSource.value.readyState === EventSource.CLOSED) {
  113. console.log('SSE 连接已关闭');
  114. } else {
  115. console.log('SSE 错误,尝试重新连接...');
  116. // 可以在这里实现自动重连逻辑
  117. connectSSE();
  118. }
  119. };
  120. };
  121. const connectWebSocket = () => {
  122. socket.value = new WebSocket(`/connect-service/ws/chat/PC_WEB_CHAT/` + loginUser.value.userId);
  123. socket.value.onopen = () => {
  124. console.log('WebSocket 已连接');
  125. isConnected.value = true;
  126. // 发送队列中的消息
  127. messageQueue.value.forEach(message => socket.value.send(message));
  128. messageQueue.value = [];
  129. startHeartbeat();
  130. };
  131. socket.value.onmessage = (event) => {
  132. // 在此处理收到的消息
  133. const socketMessage = JSON.parse(event.data);
  134. if(socketMessage.senderId != selectChatUser.value.userId) {
  135. return;
  136. }
  137. const messageType = socketMessage.type;
  138. if (messageType === 1) {
  139. // 处理对话
  140. chatMessageArr.value.push(socketMessage);
  141. }
  142. };
  143. socket.value.onerror = (error) => {
  144. isConnected.value = false;
  145. // 关闭当前连接
  146. socket.value.close();
  147. // 尝试重连
  148. setTimeout(connectWebSocket, reconnectInterval.value);
  149. };
  150. socket.value.onclose = () => {
  151. console.log('WebSocket 已关闭');
  152. isConnected.value = false;
  153. // 尝试重连
  154. setTimeout(connectWebSocket, reconnectInterval.value);
  155. };
  156. };
  157. const startHeartbeat = () => {
  158. const heartbeat = () => {
  159. if (isConnected.value) {
  160. socket.value.send('ping'); // 发送心跳包
  161. }
  162. };
  163. setInterval(heartbeat, heartbeatInterval.value);
  164. };
  165. const sendMessage = (message) => {
  166. if (isConnected.value) {
  167. socket.value.send(message);
  168. } else {
  169. messageQueue.value.push(message);
  170. }
  171. };
  172. const handleInput = (event) => {
  173. inputValue.value = event.target.value;
  174. };
  175. const handleSendMessage = () => {
  176. if (event.key === 'Enter') {
  177. event.preventDefault(); // 阻止默认的换行行为
  178. // 消息发送
  179. if (running.value) {
  180. message.error("对话进行中...");
  181. return;
  182. }
  183. if (inputValue.value.trim() !== '') {
  184. const meMessage = JSON.stringify({
  185. senderId: loginUser.value.userId,
  186. receiverId: selectChatUser.value.userId,
  187. type: 1,
  188. time: new Date().getTime(),
  189. message: inputValue.value.trim()
  190. })
  191. const robotMsg = JSON.stringify({
  192. senderId: selectChatUser.value.userId,
  193. receiverId: loginUser.value.userId,
  194. type: 1,
  195. time: new Date().getTime(),
  196. message: ''
  197. })
  198. running.value = true;
  199. robotMessage.value = '';
  200. // 当前用户自己发送的消息直接提交到页面渲染
  201. chatMessageArr.value.push(JSON.parse(meMessage));
  202. chatMessageArr.value.push(JSON.parse(robotMsg));
  203. // ws发送消息
  204. sendMessage(meMessage);
  205. // 清空输入框
  206. inputValue.value = '';
  207. }
  208. }
  209. }
  210. // 定义滚动到底部函数
  211. function scrollToBottom() {
  212. nextTick(() => {
  213. // 确保 DOM 更新完成后再执行滚动操作
  214. if (chatCoreContainerRef.value) {
  215. chatCoreContainerRef.value.scrollTop = chatCoreContainerRef.value.scrollHeight;
  216. }
  217. });
  218. }
  219. // 监听消息数组的变化,自动滚动到底部
  220. watch(chatMessageArr, () => {
  221. scrollToBottom();
  222. }, { deep: true });
  223. onMounted(() => {
  224. connectWebSocket();
  225. connectSSE();
  226. });
  227. onUnmounted(() => {
  228. });
  229. return {
  230. md,
  231. running,
  232. robotMessage,
  233. inputValue,
  234. isConnected,
  235. selectChatUser,
  236. messageQueue,
  237. loginUser,
  238. reconnectInterval,
  239. heartbeatInterval,
  240. chatMessageArr,
  241. chatCoreContainerRef,
  242. handleSendMessage,
  243. handleInput,
  244. connectWebSocket,
  245. connectSSE,
  246. scrollToBottom
  247. };
  248. }
  249. });
  250. </script>
  251. <style scoped>
  252. .chat-button-group {
  253. position: absolute;
  254. left: 0px;
  255. top: 0px;
  256. display: flex;
  257. justify-content: center;
  258. align-items: center;
  259. margin-top: 10px;
  260. }
  261. .chat-button-group button {
  262. padding-top: 3px;
  263. margin-right: 15px;
  264. height: 30px;
  265. width: 30px;
  266. border-radius: 50%;
  267. background-color: transparent;
  268. }
  269. .chat-right-header-container {
  270. position: absolute;
  271. top: 0px;
  272. left: 0px;
  273. width: 100%;
  274. height: 50px;
  275. line-height: 50px;
  276. padding: 0px 20px;
  277. border-bottom: 1px solid #e3e3e3;
  278. }
  279. .chat-editer-menu-container {
  280. position: absolute;
  281. bottom: 150px;
  282. left: 0px;
  283. width: 100%;
  284. height: 50px;
  285. line-height: 50px;
  286. font-size: 13px;
  287. text-indent: 20px;
  288. color: #666;
  289. border-top: 1px solid #e3e3e3;
  290. }
  291. .chat-editer-container {
  292. position: absolute;
  293. bottom: 0px;
  294. left: 0px;
  295. width: 100%;
  296. height: 150px;
  297. }
  298. .chat-core-container {
  299. position: absolute;
  300. top: 50px;
  301. left: 0px;
  302. width: 100%;
  303. height: calc(100% - 249px);
  304. padding: 10px 30px;
  305. overflow-y: scroll;
  306. overflow-x: hidden;
  307. }
  308. .chat-input {
  309. position: absolute;
  310. top: 0px;
  311. left: 0px;
  312. padding: 20px;
  313. width: 100%;
  314. height: 100%;
  315. line-height: normal;
  316. font-size: 15px;
  317. resize: none;
  318. border: none;
  319. outline: none;
  320. box-shadow: none;
  321. }
  322. .chat-message-card {
  323. display: flex;
  324. margin: 10px 0;
  325. }
  326. .my-message {
  327. justify-content: flex-end;
  328. }
  329. .other-message {
  330. justify-content: flex-start;
  331. }
  332. .my-message-content {
  333. margin-right: 5px;
  334. }
  335. .other-message-content {
  336. margin-left: 5px;
  337. margin-top: 0px;
  338. }
  339. .other-message-content p {
  340. margin-bottom: 0px;
  341. }
  342. .message-avatar {
  343. position: relative;
  344. margin-right: 10px;
  345. }
  346. .message-avatar img {
  347. width: 35px;
  348. height: 35px;
  349. border-radius: 5px;
  350. }
  351. .message-content {
  352. line-height: 35px;
  353. padding: 0px 10px;
  354. border-radius: 4px;
  355. max-width: 70%;
  356. }
  357. .my-avatar {
  358. float: right;
  359. margin-left: 10px;
  360. }
  361. .chat-core-container::-webkit-scrollbar {/*滚动条整体样式*/
  362. width: 0px; /*高宽分别对应横竖滚动条的尺寸*/
  363. height: 0px;
  364. }
  365. .chat-core-container::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
  366. border-radius: 10px;
  367. background-color: transparent;
  368. background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
  369. }
  370. .chat-core-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
  371. -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
  372. /*border-radius: 10px;*/
  373. background: #EDEDED;
  374. }
  375. .message-content >>> img {
  376. width: 200px;
  377. margin-top: 10px;
  378. }
  379. .chat-mess-triangle-left {
  380. position: absolute;
  381. right: -20px;
  382. top: 10px;
  383. width: 0;
  384. height: 0;
  385. border-top: 8px solid transparent;
  386. border-right: 16px solid #f5f5f5;
  387. border-bottom: 8px solid transparent;
  388. }
  389. .chat-mess-triangle-right {
  390. position: absolute;
  391. left: -20px;
  392. top: 10px;
  393. width: 0;
  394. height: 0;
  395. border-top: 8px solid transparent;
  396. border-left: 16px solid #a9ea7a;
  397. border-bottom: 8px solid transparent;
  398. }
  399. </style>