commentBox.vue 15 KB

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