broadcast.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. <template>
  2. <view class="container">
  3. <view class="top">
  4. <view id="aliplay" class="course-video" v-if="liveInfo.is_play"></view>
  5. <view class="course-video errorContent" v-else @touchmove.prevent ref="column">
  6. <view class="contentText">
  7. <image class="tvImg" src="../../static/img/tv.png" mode="widthFix"></image>
  8. <view v-if="liveInfo.live_status == 0">直播即将开始</view>
  9. <view v-else-if="liveInfo.live_status == 2">直播已结束</view>
  10. <view v-else-if="liveInfo.live_status == 1">讲师离开一会~马上回来</view>
  11. <view v-else-if="liveInfo.live_status == -1" v-text="live_error">讲师离开一会~马上回来</view>
  12. </view>
  13. </view>
  14. </view>
  15. <view class="uni-column">
  16. <view class="content" id="content" :style="{ height: style.contentViewHeight + 'px' }">
  17. <scroll-view
  18. id="scrollview"
  19. class="Meslist"
  20. :scroll-y="true"
  21. :style="{ height: style.contentViewHeight + 'px' }"
  22. :scroll-with-animation="true"
  23. :scroll-top="scrollTop"
  24. >
  25. <view id="listBox">
  26. <view v-for="(ls, index) in MsgList" :key="index">
  27. <view class="m-item" :id="'message' + index">
  28. <view class="m-left"><image class="head_icon" :src="ls.avatar" v-if="ls.uid != userInfo.uid"></image></view>
  29. <view class="m-content">
  30. <view class="m-content-head" :class="{ 'm-content-head-right': ls.uid == userInfo.uid }">
  31. <view :class="'m-content-head-' + 'home'" v-if="ls.uid == userInfo.uid">
  32. <rich-text :nodes="ls.content.replace(reg, emotion)"></rich-text>
  33. </view>
  34. <view :class="'m-content-head-' + 'customer'" v-if="ls.uid != userInfo.uid">
  35. <rich-text :nodes="ls.content.replace(reg, emotion)"></rich-text>
  36. </view>
  37. </view>
  38. </view>
  39. <view class="m-right"><image class="head_icon" :src="userInfo.avatar" v-if="ls.uid == userInfo.uid"></image></view>
  40. </view>
  41. </view>
  42. </view>
  43. </scroll-view>
  44. </view>
  45. <view class="foot">
  46. <view class="footheight">
  47. <view class="footer">
  48. <view class="footer-emotion" @tap="show"><text class="icon iconfont icon-face"></text></view>
  49. <view class="footer-center"><input class="input-text" placeholder="清输入您的信息" type="text" v-model="inputValue" @focus="foc" :focus="focus" /></view>
  50. <view class="footer-right" @tap="sendMessge">
  51. <view id="msg-type"><image src="/static/img/img024.png"></image></view>
  52. </view>
  53. </view>
  54. <emotion @emotion="handleEmotion" :height="200" v-if="showPannel"></emotion>
  55. </view>
  56. </view>
  57. </view>
  58. </view>
  59. </template>
  60. <script>
  61. import { mapState, mapMutations } from 'vuex';
  62. import messageShow from '@/components/imchat-emoji/messageshow.vue';
  63. import Emotion from '@/components/imchat-emoji/Emotion/index.vue';
  64. import { liveList } from '@/api/live.js';
  65. // #ifdef H5
  66. import { getWwwJs } from '@/utils/rocessor.js';
  67. // #endif
  68. export default {
  69. components: {
  70. messageShow,
  71. Emotion
  72. },
  73. data() {
  74. return {
  75. live_id: '', //直播id
  76. special_id: '', //专题id
  77. style: {
  78. pageHeight: 0,
  79. contentViewHeight: 0,
  80. footViewHeight: 90,
  81. mitemHeight: 0
  82. },
  83. scrollTop: 0,
  84. messages: '', //消息内容
  85. inputValue: '', //输入框内容
  86. showPannel: false,
  87. focus: false,
  88. // socketTask: null,
  89. socketOpen: false,
  90. reg: /\#[\S]{1,3}\;/gi,
  91. PullUrl: '',
  92. socketTask: '', //保存长连接会话
  93. player: '', //绑定播放器
  94. // 直播详情
  95. liveInfo: {
  96. is_play: false
  97. },
  98. MsgList: []
  99. };
  100. },
  101. computed: {
  102. ...mapState(['userInfo'])
  103. },
  104. onLoad: function(option) {
  105. this.info(option);
  106. },
  107. methods: {
  108. info(option) {
  109. let obj = this;
  110. obj.live_id = option.live_id;
  111. obj.special_id = option.special_id;
  112. const res = uni.getSystemInfoSync();
  113. obj.style.pageHeight = res.windowHeight;
  114. obj.style.contentViewHeight = res.windowHeight - (uni.getSystemInfoSync().screenWidth / 750) * 620; //像素
  115. obj.connectSocketInit();
  116. obj.loadDate().then(e => {
  117. obj.$nextTick(function() {
  118. // #ifdef H5
  119. // 加載網絡js資源
  120. obj.aliPlayShow();
  121. // #endif
  122. });
  123. });
  124. },
  125. // #ifdef H5
  126. // 渲染播放器
  127. aliPlayShow() {
  128. this.player = new Aliplayer(
  129. {
  130. id: 'aliplay',
  131. source: this.PullUrl,
  132. width: '100%',
  133. height: '56.25vw',
  134. autoplay: true,
  135. isLive: true,
  136. rePlay: false,
  137. playsinline: true,
  138. preload: true,
  139. autoPlayDelay: '',
  140. controlBarVisibility: 'hover',
  141. useH5Prism: true,
  142. skinLayout: [
  143. {
  144. name: 'errorDisplay',
  145. align: 'tlabs',
  146. x: 0,
  147. y: 0
  148. },
  149. {
  150. name: 'infoDisplay'
  151. },
  152. {
  153. name: 'controlBar',
  154. align: 'blabs',
  155. x: 0,
  156. y: 0,
  157. children: [
  158. {
  159. name: 'liveDisplay',
  160. align: 'tlabs',
  161. x: 15,
  162. y: 6
  163. },
  164. {
  165. name: 'fullScreenButton',
  166. align: 'tr',
  167. x: 10,
  168. y: 10
  169. },
  170. {
  171. name: 'volume',
  172. align: 'tr',
  173. x: 5,
  174. y: 10
  175. }
  176. ]
  177. }
  178. ]
  179. },
  180. function(player) {
  181. console.log('The player is created');
  182. }
  183. );
  184. this.bindEvevt();
  185. },
  186. // #endif
  187. /*
  188. * 播放器错误事件处理
  189. *
  190. * */
  191. bindEvevt: function() {
  192. var that = this;
  193. this.player.on('onM3u8Retry', function() {
  194. if (that.player) that.player.dispose();
  195. that.live_error = '主播暂时离开,请稍后.';
  196. that.live_status = -1;
  197. that.liveInfo.is_play = false;
  198. console.log('主播暂时离开,请稍后......');
  199. });
  200. this.player.on('liveStreamStop', function() {
  201. console.log('直播失败或直播已结束');
  202. that.live_error = '直播失败或直播已结束';
  203. that.live_status = -1;
  204. that.liveInfo.is_play = false;
  205. if (that.player) that.player.dispose();
  206. });
  207. this.player.on('error', function(e) {
  208. //隐藏
  209. $('.prism-ErrorMessage').hide();
  210. //解析
  211. var errorData = e.paramData;
  212. that.live_error = '直播失败或直播已结束';
  213. that.live_status = -1;
  214. that.liveInfo.is_play = false;
  215. if (that.player) that.player.dispose();
  216. });
  217. },
  218. //创建一个 WebSocket 连接。
  219. connectSocketInit() {
  220. let obj = this;
  221. obj.socketTask = uni.connectSocket({
  222. url: 'ws://doctortest.igxys.com:20014',
  223. success(data) {
  224. console.log('websocket连接成功');
  225. }
  226. });
  227. // 开始打开链接监听
  228. obj.socketTask.onOpen(res => {
  229. console.log('WebSocket连接正常打开中...!');
  230. obj.socketOpen = true;
  231. // 注:只有连接正常打开中 ,才能正常收到消息
  232. obj.socketTask.onMessage(res => {
  233. console.log('监听返回事件');
  234. // 保存返回类型处理
  235. let data = JSON.parse(res.data);
  236. obj.messageType(data, 'live_ing').then(() => {
  237. location.reload();
  238. });
  239. if(data.type == 'message'){
  240. console.log(data,'data')
  241. let message={
  242. avatar:data.userInfo.avatar,
  243. content:data.message,
  244. nickname:data.userInfo.nickname,
  245. type:data.m_type,
  246. uid:data.userInfo.uid,
  247. }
  248. this.MsgList = this.MsgList.concat(message);
  249. console.log(this.MsgList)
  250. }
  251. obj.$nextTick(function() {
  252. obj.scrollToBottom();
  253. });
  254. });
  255. // 进入房间
  256. let join = `{"type":"handshake","role":"user","uid": "${obj.userInfo.uid}","room":"${obj.live_id}"}`;
  257. obj.socketTask.send({
  258. data: join,
  259. success: res => {
  260. console.log('初始化消息发送成功');
  261. }
  262. });
  263. });
  264. },
  265. // 处理判断监听回收类型
  266. messageType(res, type) {
  267. console.log(res);
  268. console.log(res.type, type);
  269. return new Promise((ok, error) => {
  270. if (res.type == type) {
  271. ok(true);
  272. }
  273. });
  274. },
  275. sendMessge: function() {
  276. var obj = this;
  277. if (obj.inputValue.trim() == '') {
  278. obj.inputValue = '';
  279. } else {
  280. //点击发送按钮
  281. this.getInputMessage(obj.inputValue);
  282. obj.inputValue = '';
  283. this.showPannel = false;
  284. }
  285. },
  286. //获取输入框数据
  287. getInputMessage: function(message) {
  288. let obj = this;
  289. if (obj.socketOpen) {
  290. let msg = `{"content":"${message}","m_type":"1","type":"send","room":"${this.live_id}"}`;
  291. console.log(msg);
  292. obj.socketTask.send({
  293. data: msg,
  294. success: res => {
  295. let newmessage = JSON.parse(msg);
  296. console.log('输入框消息发送成功');
  297. console.log(newmessage);
  298. // obj.MsgList = obj.MsgList.concat(mes);
  299. obj.$nextTick(function() {
  300. obj.scrollToBottom();
  301. });
  302. },
  303. fail: err => {
  304. console.log(err);
  305. }
  306. });
  307. } else {
  308. obj.messages = message;
  309. }
  310. obj.setScrollH();
  311. },
  312. scrollToBottom: function() {
  313. //哪里需要用到就在哪里使用
  314. let obj = this;
  315. let query = uni.createSelectorQuery().in(this);
  316. query
  317. .select('#listBox')
  318. .fields(
  319. {
  320. dataset: true,
  321. size: true,
  322. scrollOffset: true,
  323. rect: true
  324. },
  325. function(res) {
  326. obj.scrollTop = res.height;
  327. }
  328. )
  329. .exec();
  330. },
  331. // 设置高度 用emit辅助
  332. setScrollH: function() {
  333. let screenHeight = uni.getSystemInfoSync().windowHeight; //获取屏幕高度
  334. // 通过query 获取其余盒子的高度
  335. let query = uni.createSelectorQuery().in(this);
  336. query.select('.foot').boundingClientRect();
  337. // 通过query.exec返回的数组 进行减法 同时 去除margin 和border的
  338. query.exec(res => {
  339. let foot = res[0].height;
  340. screenHeight = screenHeight - foot;
  341. });
  342. },
  343. // 关闭websocket【离开这个页面的时候执行关闭】
  344. closeSocket() {
  345. this.socketTask.close({
  346. success(res) {
  347. this.socketOpen = false;
  348. console.log('关闭成功', res);
  349. },
  350. fail(err) {
  351. console.log('关闭失败', err);
  352. }
  353. });
  354. },
  355. loadDate() {
  356. let obj = this;
  357. return new Promise(function(resolve, reject) {
  358. liveList({
  359. special_id: obj.special_id,
  360. live_id: obj.live_id
  361. })
  362. .then(({ data }) => {
  363. obj.liveInfo = data.liveInfo;
  364. obj.liveInfo.is_play = obj.liveInfo.is_play == 1 ? true : false;
  365. obj.PullUrl = data.PullUrl;
  366. // 保存播放状态
  367. obj.liveInfo.live_status = data.live_status;
  368. resolve(data);
  369. obj.$nextTick(function() {
  370. obj.scrollToBottom();
  371. });
  372. })
  373. .catch(e => {
  374. reject(e.message);
  375. if(e.message == '您还没有支付请支付后再进行观看'){
  376. uni.navigateTo({
  377. url:'/pages/live/details?id='+obj.special_id+'&type=free'
  378. })
  379. }
  380. });
  381. });
  382. },
  383. //语音识别
  384. // startRecognize: function() {
  385. // var options = {};
  386. // var that = this;
  387. // options.engine = 'iFly';
  388. // that.inputValue = '';
  389. // plus.speech.startRecognize(
  390. // options,
  391. // function(s) {
  392. // console.log(s);
  393. // that.inputValue += s;
  394. // },
  395. // function(e) {
  396. // console.log('语音识别失败:' + e.message);
  397. // }
  398. // );
  399. // },
  400. show() {
  401. this.showPannel = !this.showPannel;
  402. },
  403. // 光标触发隐藏表情
  404. foc() {
  405. this.showPannel = false;
  406. },
  407. handleEmotion(i) {
  408. this.inputValue += i;
  409. },
  410. emotion(res) {
  411. let word = res.replace(/\#|\;/gi, '');
  412. const list = [
  413. '微笑',
  414. '撇嘴',
  415. '色',
  416. '发呆',
  417. '得意',
  418. '流泪',
  419. '害羞',
  420. '闭嘴',
  421. '睡',
  422. '大哭',
  423. '尴尬',
  424. '发怒',
  425. '调皮',
  426. '呲牙',
  427. '惊讶',
  428. '难过',
  429. '酷',
  430. '冷汗',
  431. '抓狂',
  432. '吐',
  433. '偷笑',
  434. '可爱',
  435. '白眼',
  436. '傲慢',
  437. '饥饿',
  438. '困',
  439. '惊恐',
  440. '流汗',
  441. '憨笑',
  442. '大兵',
  443. '奋斗',
  444. '咒骂',
  445. '疑问',
  446. '嘘',
  447. '晕',
  448. '折磨',
  449. '衰',
  450. '骷髅',
  451. '敲打',
  452. '再见',
  453. '擦汗',
  454. '抠鼻',
  455. '鼓掌',
  456. '糗大了',
  457. '坏笑',
  458. '左哼哼',
  459. '右哼哼',
  460. '哈欠',
  461. '鄙视',
  462. '委屈',
  463. '快哭了',
  464. '阴险',
  465. '亲亲',
  466. '吓',
  467. '可怜',
  468. '菜刀',
  469. '西瓜',
  470. '啤酒',
  471. '篮球',
  472. '乒乓',
  473. '咖啡',
  474. '饭',
  475. '猪头',
  476. '玫瑰',
  477. '凋谢',
  478. '示爱',
  479. '爱心',
  480. '心碎',
  481. '蛋糕',
  482. '闪电',
  483. '炸弹',
  484. '刀',
  485. '足球',
  486. '瓢虫',
  487. '便便',
  488. '月亮',
  489. '太阳',
  490. '礼物',
  491. '拥抱',
  492. '强',
  493. '弱',
  494. '握手',
  495. '胜利',
  496. '抱拳',
  497. '勾引',
  498. '拳头',
  499. '差劲',
  500. '爱你',
  501. 'NO',
  502. 'OK',
  503. '爱情',
  504. '飞吻',
  505. '跳跳',
  506. '发抖',
  507. '怄火',
  508. '转圈',
  509. '磕头',
  510. '回头',
  511. '跳绳',
  512. '挥手',
  513. '激动',
  514. '街舞',
  515. '献吻',
  516. '左太极',
  517. '右太极'
  518. ];
  519. let index = list.indexOf(word);
  520. return `<img src="https://res.wx.qq.com/mpres/htmledition/images/icon/emotion/${index}.gif" align="middle">`;
  521. }
  522. }
  523. };
  524. </script>
  525. <!-- #ifdef H5 -->
  526. <style lang="postcss" scoped>
  527. @import 'https://g.alicdn.com/de/prismplayer/2.7.2/skins/default/aliplayer-min.css';
  528. </style>
  529. <!-- #endif -->
  530. <style lang="scss">
  531. .container{
  532. background-color: #FFFFFF;
  533. }
  534. //视频
  535. .top {
  536. width: 100%;
  537. height: 500rpx;
  538. .course-video {
  539. width: 100%;
  540. height: 60vw;
  541. &.errorContent {
  542. display: flex;
  543. justify-content: center;
  544. align-items: center;
  545. background-color: #212121;
  546. text-align: center;
  547. .contentText {
  548. font-size: $font-lg;
  549. color: #ffffff;
  550. .tvImg {
  551. width: 75rpx;
  552. height: 64rpx;
  553. margin-bottom: 25rpx;
  554. }
  555. }
  556. }
  557. }
  558. }
  559. .uni-column {
  560. display: flex;
  561. flex-direction: column;
  562. }
  563. .content {
  564. display: flex;
  565. flex: 1;
  566. /* margin-bottom: 100upx; */
  567. }
  568. .foot {
  569. box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
  570. position: fixed;
  571. width: 100%;
  572. height: auto;
  573. left: 0upx;
  574. bottom: 0;
  575. overflow: hidden;
  576. }
  577. .footer {
  578. display: flex;
  579. flex-direction: row;
  580. width: 100%;
  581. height: 110rpx;
  582. line-height: 100rpx;
  583. overflow: hidden;
  584. background-color: #ffffff;
  585. }
  586. .footer-left {
  587. width: 80upx;
  588. height: 80upx;
  589. display: flex;
  590. justify-content: center;
  591. align-items: center;
  592. }
  593. .footer-right {
  594. background-color: #6786fb;
  595. width: 130rpx;
  596. display: flex;
  597. justify-content: center;
  598. align-items: center;
  599. color: #1482d1;
  600. line-height: 20rpx;
  601. image {
  602. width: 50rpx;
  603. height: 50rpx;
  604. }
  605. }
  606. .footer-emotion {
  607. padding: 0rpx 25rpx;
  608. display: flex;
  609. justify-content: flex-end;
  610. align-items: center;
  611. color: #1482d1;
  612. }
  613. .footer-center {
  614. border-left: 2rpx solid #cccccc;
  615. padding-left: 20rpx;
  616. flex: 1;
  617. display: flex;
  618. justify-content: center;
  619. align-items: center;
  620. font-size: 26rpx;
  621. }
  622. .footer-center .input-text {
  623. font-size: 26rpx;
  624. flex: 1;
  625. background: #fff;
  626. font-family: verdana !important;
  627. overflow: hidden;
  628. border-radius: 15upx;
  629. }
  630. @font-face {
  631. font-family: 'iconfont';
  632. /* project id 1134039 */
  633. src: url('http://at.alicdn.com/t/font_1134039_uait6xu86bf.eot');
  634. src: url('http://at.alicdn.com/t/font_1134039_uait6xu86bf.eot?#iefix') format('embedded-opentype'), url('http://at.alicdn.com/t/font_1134039_uait6xu86bf.woff2') format('woff2'),
  635. url('http://at.alicdn.com/t/font_1134039_uait6xu86bf.woff') format('woff'), url('http://at.alicdn.com/t/font_1134039_uait6xu86bf.ttf') format('truetype'),
  636. url('http://at.alicdn.com/t/font_1134039_uait6xu86bf.svg#iconfont') format('svg');
  637. }
  638. .iconfont {
  639. font-family: 'iconfont' !important;
  640. font-size: 18px;
  641. font-style: normal;
  642. -webkit-font-smoothing: antialiased;
  643. -moz-osx-font-smoothing: grayscale;
  644. }
  645. .icon {
  646. color: #f3b72c !important;
  647. }
  648. .icon-face:before {
  649. content: '\e71c';
  650. font-size: 50upx;
  651. }
  652. .m-item {
  653. display: flex;
  654. flex-direction: row;
  655. padding-top: 40upx;
  656. }
  657. .m-left {
  658. display: flex;
  659. width: 120upx;
  660. justify-content: center;
  661. align-items: flex-start;
  662. }
  663. .m-content {
  664. display: flex;
  665. flex: 1;
  666. flex-direction: column;
  667. justify-content: center;
  668. word-break: break-all;
  669. font-size: 22rpx;
  670. }
  671. .m-right {
  672. display: flex;
  673. width: 120upx;
  674. justify-content: center;
  675. align-items: flex-start;
  676. }
  677. .head_icon {
  678. width: 80upx;
  679. height: 80upx;
  680. border-radius: 100%;
  681. }
  682. .m-content-head {
  683. position: relative;
  684. }
  685. .m-content-head-right {
  686. display: flex;
  687. justify-content: flex-end;
  688. }
  689. .m-content-head-customer {
  690. text-align: left;
  691. background: #f7f7fb;
  692. border: 1px #f7f7fb solid;
  693. border-radius: 20upx;
  694. padding: 20upx 25rpx;
  695. font-size: 26rpx !important;
  696. display: inline-block;
  697. }
  698. .m-content-head-customer:before {
  699. border: 15upx solid transparent;
  700. border-right: 15upx solid #f7f7fb;
  701. left: -26upx;
  702. width: 0;
  703. height: 0;
  704. position: absolute;
  705. content: ' ';
  706. }
  707. .m-content-head-home {
  708. border: 1upx white solid;
  709. font-size: 26rpx !important;
  710. background: white;
  711. border-radius: 20upx;
  712. padding: 20upx;
  713. background-color: #f7f7fb;
  714. }
  715. .m-content-head-home:after {
  716. border: 15upx solid transparent;
  717. border-left: 15upx solid #f7f7fb;
  718. top: 20upx;
  719. right: -26upx;
  720. width: 0;
  721. height: 0;
  722. position: absolute;
  723. content: ' ';
  724. }
  725. </style>