index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <template>
  2. <view class="content">
  3. <!-- <view class="v-html-click" @tap="handleClick()" ref="htmlContent" v-html="code"></view>
  4. <span v-copy="123">复制</span> -->
  5. <scroll-view scroll-y="true" :scroll-top="scroll_top" style="top: 0%;width: 100vw;height:90vh;">
  6. <view class="scroll-view-text">
  7. <view style=" margin-right: 5%; margin-left: 5%; margin-top: 3%; display: flex;user-select: text;" v-for="(msgIndex,index) in recvMsgQueue ">
  8. <!-- ai 的消息结构体 -->
  9. <view v-if="isAItell(recvMsgQueue[index].who_msg)" style="display: flex; width: 100%;height: 100%;margin-bottom: 3%;">
  10. <image :src="make_avatar(recvMsgQueue[index].who_msg)" style="width: 150upx;height: 150upx;margin-right: 1%;"></image>
  11. <view style="margin-bottom: 3%; margin-top: 1%;margin-left: 1%;padding: 1%; width: 70vw;background: #EFEFEF;border-radius: 10rpx 10rpx 10rpx 10rpx;">
  12. <!-- <text style="width: 100%;align-self: auto;">
  13. {{recvMsgQueue[index].msg_text}}
  14. </text> -->
  15. <view @tap="handleClick()" style="width: 100%;" class="htmlContent" ref="htmlContent" v-html="returnText(recvMsgQueue[index].msg_text)"></view>
  16. <view @click="stopRecv()" v-if="recvStatus&&recvMsgQueue[index].isFinish==false" style="display: flex;width: 100%;align-items: center;justify-content: center;">
  17. <image src="../../static/closed.png" style="width: 50upx;height: 50upx;align-self: center;"></image>
  18. </view>
  19. </view>
  20. </view>
  21. <!-- 我发送 的消息结构体 -->
  22. <view v-if="!isAItell(recvMsgQueue[index].who_msg)" style="display: flex; width: 100%;height: 100%;margin-bottom: 3%;">
  23. <view style="justify-content: end;display: flex; width: 100%;height: 100%; ">
  24. <view style="justify-content: space-between; display: flex;;align-items:center; padding: 1%;margin-right: 2%;background:green;border-radius: 10rpx 10rpx 10rpx 10rpx;">
  25. <view></view>
  26. <text style="align-self: auto;color: aliceblue;">
  27. {{recvMsgQueue[index].msg_text}}
  28. </text>
  29. </view>
  30. <view>
  31. <image :src="make_avatar(recvMsgQueue[index].who_msg)" style="width: 150upx;height: 150upx;right: 0%;"></image>
  32. </view>
  33. </view>
  34. </view>
  35. </view>
  36. </view>
  37. </scroll-view>
  38. <view style="display: flex;width: 100vw;height: 10vh;bottom: 0%;">
  39. <!-- <view style="background-color: gainsboro;width: 90%;">
  40. <input :disabled="disabled" placeholder-style="margin-left: 5%;" style="width: 100%;height: 10vh;" @input="onKeyInput" :value="inputValue" placeholder="请输入消息内容(使用 Enter 发送)"/>
  41. </view> -->
  42. <view style="background-color: gainsboro;width: 90%;" class="textarea-box">
  43. <textarea v-model="inputValue" :cursor-spacing="15" class="textarea" auto-height="true"
  44. @keyup.shift="onKeyup('shift')" @keydown.shift="onKeydown('shift')"
  45. @keydown.enter="onKeydown('enter')" :disabled="disabled"
  46. placeholder="请输入消息内容(使用 Enter 发送)" :maxlength="-1"
  47. placeholder-class="input-placeholder"></textarea>
  48. </view>
  49. <view style="width: 10%;position: relative;">
  50. <view v-if="recvStatus==false" style="width: 100%; height: 100%; display: flex; background-color:slategray;color: aliceblue;align-items: center;justify-content: center" @click="sendMsg">
  51. 发送
  52. </view>
  53. <view :animation="ani" :style="ani_style">
  54. <image v-if="recvStatus==true" src="../../static/apple.png" style="width: 100%;height: 100%;"></image>
  55. </view>
  56. </view>
  57. </view>
  58. </view>
  59. </template>
  60. <script>
  61. import marked from '../marked/marked.min.js';
  62. import config from './config.js';
  63. import hljs from "../highlight.js/lib/common.js";
  64. import "highlight.js/styles/atom-one-dark.css";
  65. import './v-copy.js';
  66. // 键盘的shift键是否被按下
  67. let shiftKeyPressed = false
  68. export default {
  69. components:{
  70. marked
  71. },
  72. mounted(){
  73. marked.setOptions({
  74. renderer: new marked.Renderer(),
  75. gfm: true,
  76. tables: true,
  77. breaks: false,
  78. pedantic: false,
  79. sanitize: false,
  80. smartLists: true,
  81. smartypants: false,
  82. highlight: function (code) {
  83. return hljs.highlightAuto(code).value;
  84. },
  85. langPrefix:"hljs language-"
  86. });
  87. },
  88. data() {
  89. return {
  90. code:'',
  91. title: 'Hello',
  92. disabled: false,
  93. socketOpen:false,
  94. socketMsgQueue:[],
  95. recvMsgQueue:[],
  96. sendMsgQueue:[],
  97. inputValue: '',
  98. _socketTask:null,
  99. scroll_text_:"\n",
  100. sendStatus:false,
  101. recvStatus:false,
  102. all_src_list:[],
  103. src_index:0,
  104. scroll_top:0,
  105. ani:'',
  106. ani_style:{
  107. width: "100upx",height: "100upx",rotate: 0,left:"25%",right:"25%",position:"absolute"
  108. },
  109. angle:359,
  110. }
  111. },
  112. onLoad() {
  113. this.initTcp();
  114. },
  115. methods: {
  116. // #ifdef H5 && VUE2
  117. onKeydown(keyname) {
  118. if (keyname == 'shift') {
  119. //按下了shift键
  120. shiftKeyPressed = true;
  121. }
  122. let self = this;
  123. // 按下了回车 且 之前没按下 shift
  124. if (keyname == 'enter' && !shiftKeyPressed) {
  125. this.$nextTick(() => {
  126. // self.sendMsg();
  127. })
  128. }
  129. },
  130. onKeyup(keyname) {
  131. if (keyname == 'shift') {
  132. //按下了shift键
  133. shiftKeyPressed = false;
  134. }
  135. },
  136. // #endif
  137. setCopy(content){
  138. // 使用#ifdef H5和#endif控制各端调用情况
  139. // 该方法不支持h5
  140. //#ifndef H5
  141. uni.setClipboardData({
  142. data: String(content), // 必须字符串
  143. success: function () {
  144. console.log('success');
  145. }
  146. });
  147. //#endif
  148. // h5端赋值方法,使用创建节点
  149. // #ifdef H5
  150. if (!document.queryCommandSupported('copy')) { // 兼容某些浏览器的判断
  151. console.log('该浏览器不支持')
  152. }
  153. let textarea = document.createElement("textarea")
  154. textarea.value = content
  155. textarea.readOnly = "readOnly"
  156. document.body.appendChild(textarea)
  157. textarea.select() // 选择对象
  158. textarea.setSelectionRange(0, content.length) // 核心
  159. let result = document.execCommand("copy") // 执行浏览器复制命令
  160. if (result) {
  161. uni.showToast({
  162. title: '复制成功',
  163. duration: 2000
  164. });
  165. }
  166. textarea.remove()
  167. // #endif
  168. },
  169. updateSrcList(){
  170. let self = this;
  171. let list = this.getIndexList(self.scroll_text_);
  172. for (var i = 0; i < list.length; i+=2) {
  173. let start = list[i];
  174. let end = list[i+1];
  175. self.all_src_list.push(self.scroll_text_.substring(start,end))
  176. }
  177. console.log("updateSrcList",self.all_src_list,self.scroll_text_)
  178. },
  179. stopRecv(){
  180. let self =this;
  181. self._socketTask.close()
  182. self.initTcp(true);
  183. self.finish_recv();
  184. },
  185. handleClick(e) {
  186. e = e || window.event;
  187. let target = e.target || e.srcElement;
  188. let name = target.tagName.toLowerCase();
  189. let self =this;
  190. if(name.substring(0,3)=="abc"){
  191. let name_list = name.split('_');
  192. if(name_list.length>0){
  193. let index = parseInt(name_list[1])-1;
  194. console.log("handleClick",index,self.all_src_list)
  195. self.setCopy(self.all_src_list[index])
  196. }
  197. console.log("handleClick",self.all_src_list[index])
  198. console.log(target.tagName.toLowerCase())
  199. }
  200. console.log(name.substring(0,3))
  201. },
  202. copyUpdates(){
  203. let self = this;
  204. let str = self.recvMsgQueue[self.recvMsgQueue.length-1].msg_text
  205. let key = "</code></pre>";
  206. let index = self.all_src_list.length;
  207. while(str.indexOf(key) != -1){
  208. index++;
  209. let s_tag = "</code><abc_"+index+" class=\"copy-button\">Copy</abc_"+index+"></pre>"
  210. console.log("s_tag",s_tag)
  211. str=str.replace("</code></pre>", s_tag)
  212. }
  213. // str=str.replaceAll("</code></pre>", "</code><abc class=\"copy-button\">Copy</abc></pre>")
  214. self.recvMsgQueue[self.recvMsgQueue.length-1].msg_text = str
  215. // console.log("copyUpdates",str)
  216. self.$forceUpdate();
  217. },
  218. getIndexList(box_str){
  219. let box = box_str;
  220. let boxarr=[];
  221. let pos = box.indexOf('```');
  222. while(pos>-1){
  223. boxarr.push(pos);
  224. pos= box.indexOf('```',pos+1);
  225. }
  226. return boxarr
  227. },
  228. returnText(text){
  229. return "<style> pre {position: relative;}.copy-button { position: absolute; top: 0;right: 0;padding: 4px 8px;background-color: #333;color: #fff;cursor: pointer;user-select: none;} p {line-height: 2;} li{line-height: 1.5;} table {border-collapse: collapse;width: 100%;font-family: Arial, sans-serif;font-size: 14px;}table th,table td {border: 1px solid #ddd;padding: 8px;text-align: left;}</style>"+text
  230. },
  231. // 滚动窗口以显示最新的一条消息
  232. showLastMsg() {
  233. let self = this;
  234. let container = uni.createSelectorQuery().in(this).select(".scroll-view-text");
  235. // 利用uniapp提供的接口获取可视区域的高度和滚动高度
  236. // let query=uni.createSelectorQuery()
  237. // let container=query.select('.box');
  238. container.fields({
  239. // rect:true, //是否返回节点布局位置信息{left,top,right,bottom}
  240. size:true, //是否返回节点尺寸信息{width,height}
  241. scrollOffset:true //是否返回节点滚动信息{scrollLeft,scrollTop}
  242. },(res)=>{
  243. self.scroll_top = res.height;
  244. // console.log(res)
  245. }).exec()
  246. },
  247. isAItell(type){
  248. if(type===config.type_ai){
  249. return true;
  250. }
  251. return false;
  252. },
  253. make_avatar(type){
  254. if(type===config.type_ai){
  255. return "../../static/PubImgs_avatar_avatar7.png";
  256. }
  257. return "../../static/PubImgs_avatar_avatar8.png";
  258. },
  259. isShow(text){
  260. return text!="";
  261. },
  262. initAni(){
  263. let self = this;
  264. let action = uni.createAnimation({
  265. duration: 10000,
  266. transformOrigin:"50% 50% 0",
  267. timingFunction: "ease",
  268. delay: 0
  269. })
  270. self.angle+=360;
  271. action.rotate(self.angle).step()
  272. self.ani = action.export()
  273. setTimeout(self.initAni,10000)
  274. },
  275. tryReConnect(){
  276. let self = this;
  277. uni.showLoading({
  278. title:"正在尝试重新连接"
  279. })
  280. self.disabled = false;
  281. self.socketOpen = false;
  282. self.initTcp()
  283. },
  284. initTcp(isStopRecv=false){
  285. let self = this;
  286. self._socketTask = new WebSocket("ws://47.88.86.123:8000/ws") ;
  287. self._socketTask.onopen = function (res) {
  288. console.log('WebSocket连接已打开!');
  289. if(!isStopRecv){
  290. uni.showLoading({
  291. title:"登录成功"
  292. })
  293. }
  294. self.socketMsgQueue = [];
  295. uni.hideLoading()
  296. self.socketOpen = true;
  297. };
  298. self._socketTask.onclose = (res)=>{
  299. // uni.showModal({
  300. // showCancel:false,
  301. // title:"WebSocket is onclose "
  302. // })
  303. console.log("WebSocket is onclose")
  304. self._socketTask.close()
  305. self.tryReConnect()
  306. };
  307. self._socketTask.onerror= function (res) {
  308. // uni.showModal({
  309. // showCancel:false,
  310. // title:"WebSocket连接打开失败,请检查"
  311. // })
  312. console.log('WebSocket连接打开失败,请检查!');
  313. };
  314. self._socketTask.onmessage = function (res) {
  315. // console.log('收到服务器内容:' + res.data);
  316. let content = ''
  317. let data = JSON.parse(res.data);
  318. content = data.content+''
  319. if(self.sendStatus){
  320. self.sendStatus = false;
  321. // content = '▪ ';
  322. self.scroll_text_ = '';
  323. // let index = self.recvMsgQueue.length>0?self.recvMsgQueue.length:0
  324. self.recvMsgQueue.push({who_msg:config.type_ai,msg_type:0,msg_text:'',isFinish:false})
  325. self.startUpdateView()
  326. }
  327. // 在dom渲染完毕后 使聊天窗口滚动到最后一条消息
  328. self.$nextTick(() => {
  329. self.showLastMsg()
  330. })
  331. // console.log('收到服务器内容:' +content );
  332. let finish_reason = data.finish_reason+''
  333. if(content!='null'){
  334. self.scroll_text_+=content;
  335. // if(content=='.'||content=='。'){
  336. // self.scroll_text_+='\n▪ ';
  337. // }
  338. }
  339. if(finish_reason!='null'){
  340. self.finish_recv()
  341. // self.recvStatus = false;
  342. // self.disabled = false;
  343. // clearTimeout(self.initAni)
  344. // self.scroll_text_+="\n"
  345. // console.log("self.recvMsgQueue",self.scroll_text_)
  346. // self.stopUpdateView();
  347. // self.copyUpdates();
  348. // self.updateSrcList();
  349. // self.recvMsgQueue[self.recvMsgQueue.length-1].isFinish = true;
  350. }else{
  351. self.recvMsgQueue[self.recvMsgQueue.length-1].msg_text = marked.parse(self.scroll_text_);
  352. }
  353. // console.log("self.recvMsgQueue.length",self.recvMsgQueue.length)
  354. };
  355. },
  356. async finish_recv(){
  357. let self = this;
  358. self.recvStatus = false;
  359. self.disabled = false;
  360. clearTimeout(self.initAni)
  361. self.scroll_text_+="\n"
  362. console.log("self.recvMsgQueue",self.scroll_text_)
  363. self.stopUpdateView();
  364. self.copyUpdates();
  365. self.updateSrcList();
  366. self.recvMsgQueue[self.recvMsgQueue.length-1].isFinish = true;
  367. },
  368. async sendMsg(){
  369. // 如果内容为空
  370. if (!this.inputValue) {
  371. // 弹出提示框
  372. return uni.showToast({
  373. // 提示内容
  374. title: '内容不能为空',
  375. // 不显示图标
  376. icon: 'none'
  377. });
  378. }
  379. this.sendSocketMessage(this.inputValue)
  380. },
  381. onKeyInput(event) {
  382. this.inputValue = event.target.value
  383. },
  384. startUpdateView(){
  385. let self = this;
  386. self.$forceUpdate();
  387. setTimeout(self.startUpdateView,500)
  388. },
  389. stopUpdateView(){
  390. let self = this;
  391. clearTimeout(self.startUpdateView)
  392. },
  393. sendSocketMessage(msg) {
  394. let self = this;
  395. if (this.socketOpen) {
  396. // uni.showLoading({
  397. // title:"正在发送"
  398. // })
  399. self.sendStatus = true;
  400. self.disabled = true;
  401. self.ani_style.rotate = 0;
  402. self.inputValue = '';
  403. self.initAni();
  404. self.recvMsgQueue.push({who_msg:config.type_self,msg_type:0,msg_text:msg,isFinish:false})
  405. self.recvStatus = true;
  406. self._socketTask.send(msg)
  407. } else {
  408. this.socketMsgQueue.push(msg);
  409. }
  410. },
  411. }
  412. }
  413. </script>
  414. <style>
  415. .content {
  416. height: 100vh;
  417. display: flex;
  418. flex-direction: column;
  419. align-items: center;
  420. justify-content: center;
  421. /* position: relative; */
  422. }
  423. .text-area {
  424. display: flex;
  425. justify-content: center;
  426. }
  427. .title {
  428. font-size: 36rpx;
  429. color: #8f8f94;
  430. }
  431. .textarea-box,
  432. .textarea {
  433. width: 100%;
  434. height: 100%;
  435. },
  436. textarea-box {
  437. width: 100% !important;
  438. }
  439. .textarea-box,
  440. .textarea,
  441. textarea,
  442. textarea-box {
  443. /* height: 120px; */
  444. }
  445. .textarea-box {
  446. background-color: #FFF;
  447. }
  448. </style>