commentBox.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. <template>
  2. <view>
  3. <template v-if="openCommentStatus==2">
  4. </template>
  5. <view class="container-body" id="msglist" v-if="openCommentStatus==1">
  6. <view class="listbox" v-for="(item, index) in msgs" :key="index" :id="'view' + index">
  7. <text :class="userId&&item.userId == userId?'list-name my':'list-name'">
  8. {{userId&&item.userId == userId ? '我' : item.nickName||'--'}}:
  9. </text>
  10. <text class="list-con">{{item.content||''}}</text>
  11. </view>
  12. <view class="empty" v-if="msgs&&msgs.length==0">暂无评论~</view>
  13. </view>
  14. </view>
  15. </template>
  16. <script>
  17. import { saveMsg,revokeMsg,getComments} from "@/api/course.js"
  18. export default {
  19. props: {
  20. height:{
  21. type: String,
  22. default:'0px'
  23. },
  24. urlOption:{
  25. type: Object,
  26. default:{}
  27. },
  28. time:{
  29. type: [String,Number],
  30. default: 0
  31. },
  32. viewCommentNum:{
  33. type: [String,Number],
  34. default: 200
  35. },
  36. openCommentStatus:{
  37. type: [String,Number],
  38. default: 3
  39. },
  40. // 用户自己开启关闭弹幕展示 1,展示弹幕,0 关闭的弹幕
  41. showDanmu:{
  42. type: [String,Number],
  43. default: 1
  44. },
  45. },
  46. data() {
  47. return {
  48. statusBarHeight: uni.getSystemInfoSync().statusBarHeight,
  49. scrollTop: 0,
  50. inputText:"",
  51. isSocketOpen:false,
  52. socket:null,
  53. isSend:true,
  54. commentList:[],
  55. msgs: [],
  56. pageNum: 1,
  57. pageSize: 10,
  58. userInfo: {},
  59. userId: '',
  60. pingpangTimes:null,
  61. // 弹幕
  62. danmuList: [],
  63. tracks:[],
  64. activeDanmus:[],
  65. flagTime: 0,
  66. danmuItemStyle:{
  67. color: '#ffffff',
  68. fontSize: '16px',
  69. border: 'solid 1px #ffffff',
  70. borderRadius: '5px',
  71. padding: '2px 2px',
  72. backgroundColor: 'rgba(255, 255, 255, 0.1)'
  73. },
  74. ctx: null,
  75. danmuIndex:{}
  76. }
  77. },
  78. mounted() {
  79. this.getComments()
  80. this.getUser();
  81. this.initTracks()
  82. if(!this.socket || !this.isSocketOpen) {
  83. this.initSocket()
  84. }
  85. },
  86. methods: {
  87. back() {
  88. uni.navigateBack()
  89. },
  90. getUser() {
  91. const userInfo = uni.getStorageSync('userInfo');
  92. if(userInfo&&JSON.stringify(userInfo)!='{}') {
  93. this.userInfo = JSON.parse(userInfo)
  94. this.userId = this.userInfo.userId || ''
  95. }else {
  96. this.userInfo = {}
  97. this.userId = ''
  98. }
  99. },
  100. getComments() {
  101. let that = this
  102. getComments({
  103. pageNum: this.openCommentStatus==2 ? 1 : this.pageNum,
  104. pageSize: this.openCommentStatus==2 ? this.viewCommentNum : this.pageSize,
  105. courseId: this.urlOption.courseId,
  106. videoId: this.urlOption.videoId,
  107. openCommentStatus: this.openCommentStatus
  108. }).then(res=>{
  109. if(res.code==200){
  110. if(this.openCommentStatus==2) {
  111. this.danmuList = res.data.list.map(item=>({
  112. commentId: item.commentId,
  113. content: item.content,
  114. time: item.time || this.time,
  115. color: "#FFFFFF",
  116. mode: item.mode || "scroll",
  117. top: null,
  118. style: {
  119. color: item.isColor==1 ? item.color || this.danmuItemStyle.color : this.danmuItemStyle.color,//是否彩色1是0否
  120. fontSize: item.fontSize || this.danmuItemStyle.fontSize,
  121. padding: this.danmuItemStyle.padding,
  122. border:this.userInfo.userId ==item.userId ? item.color ? `solid 1px ${item.color}`: this.danmuItemStyle.border : 'none',
  123. borderRadius: this.userInfo.userId==item.userId ? this.danmuItemStyle.borderRadius : 0,
  124. backgroundColor: this.userInfo.userId==item.userId ? this.danmuItemStyle.backgroundColor : 'transparent'
  125. },
  126. }))
  127. this.initDanmuIndex()
  128. that.$emit('getMore',0)
  129. } else if(this.openCommentStatus==1) {
  130. this.danmuList = []
  131. this.activeDanmus = []
  132. this.danmuIndex = {};
  133. let list = res.data.list.reverse()
  134. if (that.pageNum == 1) {
  135. that.commentList = list;
  136. that.handleScrollBottom();
  137. } else {
  138. that.commentList = that.commentList.concat(list);
  139. }
  140. that.msgs = [...list,...that.msgs]
  141. if(that.commentList.length >= res.data.total || that.commentList.length >= Number(this.viewCommentNum||200)) {
  142. that.$emit('getMore',1)
  143. } else {
  144. this.pageNum++
  145. that.$emit('getMore',0)
  146. }
  147. } else {
  148. that.danmuList = []
  149. that.activeDanmus = []
  150. that.danmuIndex = {};
  151. that.commentList = [];
  152. that.msgs = that.msgs;
  153. that.$emit('getMore',0);
  154. }
  155. }
  156. else{
  157. that.danmuList = []
  158. that.danmuIndex = {};
  159. that.commentList = [];
  160. that.msgs = that.msgs;
  161. that.$emit('getMore',0);
  162. }
  163. })
  164. },
  165. saveMsg() {
  166. if (this.inputText == "" || this.inputText.trim() == "") {
  167. uni.showToast({
  168. title: '请输入评论',
  169. icon: "none"
  170. })
  171. return;
  172. }
  173. if (!this.isSend) {
  174. return;
  175. }
  176. const param = {
  177. userId: this.userId || '',
  178. userType: 2, // 1-管理员,2-用户
  179. courseId: this.urlOption.courseId,
  180. videoId: this.urlOption.videoId,
  181. type:1, // 评论类型 1:评论,2:回复,目前没有回复,默认传1就行了
  182. content: this.inputText,
  183. time: this.time,
  184. fontSize: '16px',
  185. mode: "scroll",
  186. color: "#ffffff",
  187. }
  188. saveMsg(param).then(res=>{
  189. if(res.code == 200) {
  190. const status = res.status ? 0 : 1
  191. this.sendMsg(param,status);
  192. } else {
  193. uni.showToast({
  194. title: res.msg,
  195. icon: "none"
  196. })
  197. }
  198. })
  199. },
  200. handleInput(val) {
  201. this.inputText = val
  202. if(!this.isSocketOpen) {
  203. // 重新发起会话
  204. this.initSocket('reStart')
  205. } else {
  206. this.saveMsg();
  207. }
  208. },
  209. handleScrollBottom() {
  210. setTimeout(() => {
  211. const query = uni.createSelectorQuery().in(this);
  212. query.select('#msglist')
  213. .boundingClientRect((res) => {
  214. if(res) {
  215. const scrollH = res.height;
  216. this.scrollTop = res.height;
  217. this.$emit('getScrollTop',this.scrollTop)
  218. }
  219. }).exec();
  220. },500);
  221. },
  222. initSocket(type) {
  223. //创建一个socket连接
  224. var userId = this.userInfo.userId;
  225. var that = this;
  226. if (this.socket) {
  227. this.socket.close()
  228. this.socket = null;
  229. }
  230. this.socket = uni.connectSocket({
  231. url: getApp().globalData.wsUrl + "/app/webSocket/" + userId,
  232. multiple: true,
  233. success: res => {
  234. console.log('WebSocket连接已打开1!');
  235. that.isSocketOpen = true
  236. // 保持心跳
  237. if(that.pingpangTimes) {
  238. clearInterval(that.pingpangTimes)
  239. that.pingpangTimes= null
  240. }
  241. that.pingpangTimes=setInterval(()=>{
  242. let data={
  243. userId: that.userId || '',
  244. userType: 2, // 1-管理员,2-用户
  245. courseId: that.urlOption.courseId,
  246. videoId: that.urlOption.videoId,
  247. type:1, // 评论类型 1:评论,2:回复,目前没有回复,默认传1就行了
  248. // msg: that.inputText,
  249. cmd:'heartbeat'
  250. };
  251. that.socket.send({
  252. data: JSON.stringify(data),
  253. success: () => {
  254. // console.log('WebSocket发送心条数据!');
  255. },
  256. fail: () => {
  257. that.isSocketOpen=false
  258. }
  259. });
  260. },15000)
  261. },
  262. error: res => {
  263. console.log(res)
  264. },
  265. })
  266. this.socket.onMessage((res) => {
  267. // console.log("收到消息parse",JSON.parse(res.data))
  268. const redata = JSON.parse(res.data);
  269. if(redata.cmd=="heartbeat"){
  270. //心跳
  271. // console.log("heartbeat")
  272. }else if(redata.cmd=="sendMsg"){
  273. that.isSend=true;
  274. that.addMsg(redata);
  275. }
  276. })
  277. //监听socket打开
  278. this.socket.onOpen(() => {
  279. console.log('WebSocket连接已打开2!');
  280. that.isSocketOpen = true
  281. that.isSend = true;
  282. if(type=='reStart') {
  283. // 重连的时候重新发消息
  284. this.saveMsg()
  285. }
  286. })
  287. //监听socket关闭
  288. this.socket.onClose(() => {
  289. that.isSocketOpen = false
  290. that.socket = null
  291. console.log('WebSocket连接已关闭!');
  292. if(that.pingpangTimes) {
  293. clearInterval(that.pingpangTimes)
  294. that.pingpangTimes= null
  295. }
  296. })
  297. //监听socket错误
  298. this.socket.onError((err) => {
  299. console.log("socket err:",err)
  300. that.isSocketOpen = false
  301. that.socket = null
  302. if(that.pingpangTimes) {
  303. clearInterval(that.pingpangTimes)
  304. that.pingpangTimes= null
  305. }
  306. })
  307. },
  308. sendMsg(param,status) {
  309. if(status == 1) {
  310. this.isSend = true;
  311. this.addMsg({msg: param.content,time: param.time},2)
  312. return
  313. }
  314. if (this.isSocketOpen) {
  315. var userId = this.userInfo.userId;
  316. var data = {
  317. userId: this.userId || '',
  318. userType: 2, // 1-管理员,2-用户
  319. courseId: this.urlOption.courseId,
  320. videoId: this.urlOption.videoId,
  321. type:1, // 评论类型 1:评论,2:回复,目前没有回复,默认传1就行了
  322. msg: param.content,
  323. cmd: 'sendMsg',
  324. time: param.time,
  325. fontSize: '16px',
  326. mode: "scroll",
  327. color: "#ffffff",
  328. };
  329. this.socket.send({
  330. data: JSON.stringify(data),
  331. success: () => {
  332. console.log("发送成功")
  333. this.isSend = false;
  334. },
  335. fail: () => {
  336. console.log("发送失败")
  337. }
  338. });
  339. }
  340. },
  341. addMsg(data,type) {
  342. let obj = {}
  343. if (type==2) {
  344. obj = {
  345. content: data.msg,
  346. courseId: this.urlOption.courseId,
  347. type: 1,
  348. userId: this.userId,
  349. userType: 2,
  350. videoId: this.urlOption.videoId,
  351. nickName: '',
  352. time: data.time,
  353. fontSize: data.fontSize,
  354. mode: data.mode,
  355. color: data.color,
  356. }
  357. } else {
  358. obj = {
  359. content: data.msg,
  360. courseId: this.urlOption.courseId,
  361. type: data.type,
  362. userId: data.userId,
  363. userType: data.userType,
  364. videoId: this.urlOption.videoId,
  365. nickName: data.nickName,
  366. time: data.time,
  367. fontSize: data.fontSize,
  368. mode: data.mode,
  369. color: data.color,
  370. }
  371. }
  372. if(this.openCommentStatus == 1){
  373. this.msgs.push(obj)
  374. this.handleScrollBottom();
  375. } else if(this.openCommentStatus == 2) {
  376. this.addDanmuMsg(obj)
  377. }
  378. this.inputText = ""
  379. this.$emit("setInputText")
  380. },
  381. addDanmuMsg(content) {
  382. const id = content.userId +'_' + new Date().getTime()
  383. const mystyle = {
  384. color: content.color || this.danmuItemStyle.color,
  385. fontSize: content.fontSize || this.danmuItemStyle.fontSize,
  386. border: content.color ? `solid 1px ${content.color}`: this.danmuItemStyle.border,
  387. borderRadius: this.danmuItemStyle.borderRadius,
  388. padding: this.danmuItemStyle.padding,
  389. backgroundColor: this.danmuItemStyle.backgroundColor
  390. }
  391. const otherstyle = {
  392. color: content.color || this.danmuItemStyle.color,
  393. fontSize: content.fontSize || this.danmuItemStyle.fontSize,
  394. padding: this.danmuItemStyle.padding,
  395. }
  396. const mode = content.mode || "scroll"
  397. const obj = {
  398. commentId: content.commentId || id,
  399. userId: content.userId,
  400. content: content.content,
  401. time: this.flagTime + 1,
  402. color: content.color || this.danmuItemStyle.color,
  403. style: this.userInfo.userId == content.userId ? mystyle : otherstyle,
  404. top: null
  405. }
  406. if(this.showDanmu == 0) return
  407. // 如果danmuList超过最大大小,移除旧的弹幕
  408. const maxDanmuListSize = 10000; // 设置最大大小
  409. if (this.danmuList.length >= maxDanmuListSize) {
  410. this.danmuList.shift(); // 移除最旧的弹幕
  411. }
  412. this.danmuList.push(obj);
  413. // 更新索引
  414. if (!this.danmuIndex[obj.time]) {
  415. this.danmuIndex[obj.time] = [];
  416. }
  417. this.danmuIndex[obj.time].push(obj);
  418. },
  419. closeWSocket() {
  420. if(this.socket!=null){
  421. this.socket.close()
  422. }
  423. if(this.pingpangTimes) {
  424. clearInterval(this.pingpangTimes)
  425. this.pingpangTimes= null
  426. }
  427. },
  428. initTracks() {
  429. this.tracks = [];
  430. const trackHeight = 22; // 每行高度
  431. const trackCount = 3;
  432. for (let i = 0; i < trackCount; i++) {
  433. this.tracks.push({
  434. top: i * trackHeight + 10,
  435. isFree: true,
  436. releaseTime: 0 // 轨道释放时间
  437. });
  438. }
  439. },
  440. // 获取字体高度
  441. getTextWidth(content) {
  442. if (!this.ctx) {
  443. this.ctx = uni.createCanvasContext('myCanvas')
  444. }
  445. const metrics = this.ctx.measureText(content)
  446. return Math.ceil(metrics.width)
  447. },
  448. // 分配轨道
  449. getFreeTrack(item) {
  450. const screenWidth = uni.getSystemInfoSync().screenWidth;
  451. const width = this.getTextWidth(item.content);
  452. const passWidth = width + screenWidth;
  453. const duration = 8; // 持续时间(秒)
  454. const currentTime = Date.now();
  455. for (let i = 0; i < this.tracks.length; i++) {
  456. if (this.tracks[i].isFree || this.tracks[i].releaseTime <= currentTime) {
  457. this.tracks[i].isFree = false;
  458. this.tracks[i].releaseTime = currentTime + Math.ceil(duration * 1000 / passWidth * width) + 1000;
  459. return this.tracks[i].top;
  460. }
  461. }
  462. // 无可用轨道
  463. if (this.userInfo.userId && item.userId == this.userInfo.userId) {
  464. // console.log("自己发的弹幕");
  465. let trackHeight = this.tracks[this.tracks.length - 1].top;
  466. return Math.random() * trackHeight + 16; // 自己发的弹幕随机高度
  467. } else {
  468. // console.log("无可用轨道");
  469. return 'abandon';
  470. }
  471. },
  472. // 初始化时建立索引
  473. initDanmuIndex() {
  474. this.danmuIndex = {};
  475. this.danmuList.forEach((item) => {
  476. if (!this.danmuIndex[item.time]) {
  477. this.danmuIndex[item.time] = [];
  478. }
  479. this.danmuIndex[item.time].push(item);
  480. });
  481. },
  482. // 检测并激活弹幕
  483. checkDanmu(flagTime) {
  484. this.flagTime = flagTime;
  485. if(this.showDanmu == 0) return;
  486. const newDanmus = this.danmuList.filter((item) => Math.abs(item.time - this.flagTime) < 1);
  487. // 分配轨道高度
  488. const aliveNewDanmus = newDanmus.map((item) => {
  489. if (!item.top) {
  490. item.top = this.getFreeTrack(item);
  491. }
  492. return item;
  493. }).filter((item) => item.top !== 'abandon');
  494. // 添加到活跃列表
  495. this.activeDanmus = [...this.activeDanmus, ...aliveNewDanmus];
  496. this.$emit("getActiveDanmus",this.activeDanmus)
  497. },
  498. animationend(moveItem, i) {
  499. // 移除动画结束的弹幕(性能优化)
  500. this.activeDanmus = this.activeDanmus.filter((item) => item.commentId !== moveItem.commentId)
  501. this.$emit("getActiveDanmus",this.activeDanmus)
  502. },
  503. },
  504. beforeDestroy() {
  505. if(this.socket!=null){
  506. this.socket.close()
  507. }
  508. if(this.pingpangTimes) {
  509. clearInterval(this.pingpangTimes)
  510. this.pingpangTimes= null
  511. }
  512. }
  513. }
  514. </script>
  515. <style scoped lang="scss">
  516. @mixin u-flex($flexD, $alignI, $justifyC) {
  517. display: flex;
  518. flex-direction: $flexD;
  519. align-items: $alignI;
  520. justify-content: $justifyC;
  521. }
  522. .empty {
  523. @include u-flex(row, center, center);
  524. padding: 24rpx 50rpx;
  525. color: #999999;
  526. }
  527. .listbox {
  528. white-space: pre-wrap;
  529. letter-spacing: 1px;
  530. margin-bottom: 16rpx;
  531. }
  532. .list-name {
  533. flex-shrink: 0;
  534. font-family: PingFang SC, PingFang SC;
  535. font-weight: 600;
  536. font-size: 28rpx;
  537. color: #222222;
  538. margin-right: 16rpx;
  539. }
  540. .my {
  541. color: #FF5030;
  542. }
  543. .list-con {
  544. font-family: PingFang SC, PingFang SC;
  545. font-size: 28rpx;
  546. color: #222222;
  547. }
  548. .nav-bar {
  549. position: fixed;
  550. z-index: 9999;
  551. top: 0;
  552. left: 0;
  553. width: 100%;
  554. overflow: hidden;
  555. .nav-bg {
  556. width: 100%;
  557. height: 100%;
  558. position: absolute;
  559. left: 0;
  560. top: 0;
  561. z-index: 1;
  562. background-color: #fff;
  563. }
  564. &-box {
  565. position: relative;
  566. padding: 0 24rpx;
  567. @include u-flex(row, center, flex-start);
  568. height: 88rpx;
  569. box-sizing: border-box;
  570. z-index: 3;
  571. }
  572. &-left {
  573. width: 100%;
  574. @include u-flex(row, center, flex-start);
  575. overflow: hidden;
  576. image {
  577. flex-shrink: 0;
  578. width: 64rpx;
  579. height: 64rpx;
  580. border-radius: 12rpx 12rpx 12rpx 12rpx;
  581. }
  582. }
  583. &-name {
  584. font-family: PingFang SC, PingFang SC;
  585. font-weight: 600;
  586. font-size: 28rpx;
  587. color: #222222;
  588. }
  589. &-head {
  590. flex: 1;
  591. overflow: hidden;
  592. margin-left: 22rpx;
  593. margin-right: 22rpx;
  594. font-family: PingFang SC, PingFang SC;
  595. font-weight: 400;
  596. font-size: 23rpx;
  597. color: #999999;
  598. }
  599. }
  600. .page-bg {
  601. position: absolute;
  602. top: 0;
  603. left: 0;
  604. }
  605. .container-body {
  606. padding: 32rpx 30rpx;
  607. box-sizing: border-box;
  608. }
  609. .TUI-message-list {
  610. width: 100%;
  611. box-sizing: border-box;
  612. }
  613. .chatinput {
  614. position: fixed;
  615. left: 32rpx;
  616. right: 32rpx;
  617. z-index: 999;
  618. bottom: calc(var(--window-bottom) + 24rpx);
  619. height: 96rpx;
  620. background-color: green;
  621. background: #FFFFFF;
  622. box-shadow: 0rpx 8rpx 21rpx 0rpx rgba(0, 0, 0, 0.1);
  623. border-radius: 24rpx 24rpx 24rpx 24rpx;
  624. @include u-flex(row, center, center);
  625. padding: 0 24rpx;
  626. box-sizing: border-box;
  627. .uni-input {
  628. flex: 1;
  629. margin-right: 32rpx;
  630. font-size: 30rpx;
  631. }
  632. .send {
  633. font-family: PingFang SC, PingFang SC;
  634. font-weight: 400;
  635. font-size: 28rpx;
  636. color: #FFFFFF !important;
  637. flex-shrink: 0;
  638. padding: 0 20rpx;
  639. height: 72rpx;
  640. background: #FF5030 !important;
  641. border-radius: 8rpx 8rpx 8rpx 8rpx;
  642. &::after {
  643. border: none;
  644. }
  645. }
  646. }
  647. </style>